{"metadata":{"kernelspec":{"language":"python","display_name":"Python 3","name":"python3"},"language_info":{"name":"python","version":"3.10.12","mimetype":"text/x-python","codemirror_mode":{"name":"ipython","version":3},"pygments_lexer":"ipython3","nbconvert_exporter":"python","file_extension":".py"},"kaggle":{"accelerator":"nvidiaTeslaT4","dataSources":[{"sourceId":71549,"databundleVersionId":8561470,"sourceType":"competition"},{"sourceId":9549659,"sourceType":"datasetVersion","datasetId":5818458},{"sourceId":9552264,"sourceType":"datasetVersion","datasetId":5820241},{"sourceId":9683000,"sourceType":"datasetVersion","datasetId":5820093}],"dockerImageVersionId":30823,"isInternetEnabled":true,"language":"python","sourceType":"notebook","isGpuEnabled":true}},"nbformat_minor":4,"nbformat":4,"cells":[{"cell_type":"markdown","source":"# Lumbar Spine Classification  \n*An exploratory project for classifying lumbar spine images*  \n\nThis notebook presents a complete pipeline from data exploration to model evaluation for classifying lumbar spine images. The aim is to assist in diagnostic imaging by leveraging machine learning techniques.\n","metadata":{}},{"cell_type":"markdown","source":"## Model Pipeline and XAI Analysis\n\nIn this project, a multi-step approach is implemented to enhance both the performance and interpretability of the classification system:\n\n1. **Patch Fixation via YOLO:**  \n   The YOLO (You Only Look Once) model is utilized for effective patch fixation for each imaging modality. This step ensures that the regions of interest within the lumbar spine images are accurately localized and extracted, enabling the downstream processes to focus on the most relevant features.\n\n2. **Contrastive Learning for Classification:**  \n   For the classification task, contrastive learning is employed to learn robust feature representations. By bringing similar samples closer together and separating dissimilar ones in the feature space, the model is better equipped to distinguish between the different classes, ultimately enhancing its diagnostic accuracy.\n\n3. **XAI Analysis Using Occlusion:**  \n   To analyze model performance and understand decision-making, an occlusion-based Explainable AI (XAI) technique is applied. This approach systematically occludes parts of an image to determine their impact on the model’s predictions. While Grad-CAM is a popular choice for such interpretability tasks, its higher computational demands led to the selection of the occlusion method for this notebook implementation. This method provides a computationally efficient way to visualize the areas driving the classification decisions, facilitating a clear understanding of the model’s behavior.\n","metadata":{}},{"cell_type":"code","source":"import os\nimport re\nimport time\nimport datetime\nfrom tqdm import tqdm\nimport seaborn as sns\nimport numpy as np\nimport glob\nimport json\nimport random\nimport yaml\nfrom collections import defaultdict\nfrom tqdm import tqdm\nfrom typing import Any, Dict, List, Tuple, Union, Optional\nfrom concurrent.futures import ThreadPoolExecutor\n\nimport timm\n\nimport torch\nimport torch.nn as nn\nfrom torch.utils.data import Dataset, Subset\nfrom torch.utils.data import Subset\nfrom torchvision import transforms\nfrom PIL import Image\nfrom torch.utils.data import DataLoader\nfrom sklearn.model_selection import StratifiedKFold\n\nimport pandas as pd\nimport cv2\nimport csv\n\nfrom sklearn.model_selection import train_test_split\nfrom scipy.ndimage import gaussian_filter\n\nimport pydicom as dicom\nimport matplotlib.patches as patches\n\nimport matplotlib.pyplot as plt\nfrom matplotlib import animation, rc\nfrom pathlib import Path\nimport pandas as pd\n\nimport pydicom as dicom \nimport pydicom\nfrom pydicom.pixel_data_handlers.util import (\n    apply_voi_lut,\n    apply_color_lut,\n    apply_modality_lut,\n)","metadata":{"_uuid":"8f2839f25d086af736a60e9eeb907d3b93b6e0e5","_cell_guid":"b1076dfc-b9ad-4769-8c92-a6c4dae69d19","trusted":true,"execution":{"iopub.status.busy":"2025-04-29T12:56:23.257217Z","iopub.execute_input":"2025-04-29T12:56:23.257582Z","iopub.status.idle":"2025-04-29T12:56:32.592264Z","shell.execute_reply.started":"2025-04-29T12:56:23.257553Z","shell.execute_reply":"2025-04-29T12:56:32.591640Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## Exploratory Data Analysis","metadata":{}},{"cell_type":"code","source":"# Reads the  data\ndata_path = '/kaggle/input/rsna-2024-lumbar-spine-degenerative-classification/'\ntrain_data  = pd.read_csv(data_path + 'train.csv')\ntrain_label = pd.read_csv(data_path + 'train_label_coordinates.csv')\ntrain_description  = pd.read_csv(data_path + 'train_series_descriptions.csv')\ntest_description   = pd.read_csv(data_path + 'test_series_descriptions.csv')\nsubmission         = pd.read_csv(data_path + 'sample_submission.csv')\n\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-04-29T12:56:32.593399Z","iopub.execute_input":"2025-04-29T12:56:32.593692Z","iopub.status.idle":"2025-04-29T12:56:32.799257Z","shell.execute_reply.started":"2025-04-29T12:56:32.593665Z","shell.execute_reply":"2025-04-29T12:56:32.798307Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"train_data.head(5)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-04-29T12:56:32.800974Z","iopub.execute_input":"2025-04-29T12:56:32.801224Z","iopub.status.idle":"2025-04-29T12:56:32.828991Z","shell.execute_reply.started":"2025-04-29T12:56:32.801204Z","shell.execute_reply":"2025-04-29T12:56:32.828321Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"train_description.head(5)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-04-29T12:56:32.830371Z","iopub.execute_input":"2025-04-29T12:56:32.830691Z","iopub.status.idle":"2025-04-29T12:56:32.837717Z","shell.execute_reply.started":"2025-04-29T12:56:32.830663Z","shell.execute_reply":"2025-04-29T12:56:32.836903Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"train_label.head(5)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-04-29T12:56:32.838649Z","iopub.execute_input":"2025-04-29T12:56:32.838962Z","iopub.status.idle":"2025-04-29T12:56:32.857033Z","shell.execute_reply.started":"2025-04-29T12:56:32.838928Z","shell.execute_reply":"2025-04-29T12:56:32.856243Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# Types of modalities\nmodalities = list(train_description.iloc[:,-1].unique())\nmodality_count = train_description['series_description'].value_counts()\nplt.figure(figsize = (10,5))\nsns.barplot(x=modality_count.index, y=modality_count.values, palette=\"Set1\")\nplt.xlabel(\"Modalities\")\nplt.ylabel(\"Count\")\nplt.title(\"Distribution of Modalities\")\nplt.xticks(rotation=90)\nplt.tight_layout()\nplt.show()\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-04-29T12:56:32.857843Z","iopub.execute_input":"2025-04-29T12:56:32.858058Z","iopub.status.idle":"2025-04-29T12:56:33.164381Z","shell.execute_reply.started":"2025-04-29T12:56:32.858040Z","shell.execute_reply":"2025-04-29T12:56:33.163667Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"fig, ax = plt.subplots()\ncondition_count = train_label['condition'].value_counts()\nax.pie(condition_count, labels=condition_count.index, autopct='%1.1f%%')\nplt.axis('equal') \nplt.suptitle('Distribution of Lumbar Spine Condition') \nplt.tight_layout()\nplt.show()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-04-29T12:56:33.165100Z","iopub.execute_input":"2025-04-29T12:56:33.165362Z","iopub.status.idle":"2025-04-29T12:56:33.361837Z","shell.execute_reply.started":"2025-04-29T12:56:33.165317Z","shell.execute_reply":"2025-04-29T12:56:33.360833Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# Identify all condition columns (excluding 'study_id' or other non-relevant columns)\ncondition_columns = [col for col in train_data.columns if col not in ['study_id']]\nseverity_counts = train_data[condition_columns].stack().value_counts()\nplt.figure(figsize=(10,5))\nsns.barplot(x=severity_counts.index, y=severity_counts.values, palette=\"Set1\")\nplt.xlabel(\"Severity Levels\")\nplt.ylabel(\"Count\")\nplt.title(\"Distribution of Severity Levels Across All Lumbar Spine Conditions\")\nplt.xticks(rotation=90)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-04-29T12:56:33.362660Z","iopub.execute_input":"2025-04-29T12:56:33.363018Z","iopub.status.idle":"2025-04-29T12:56:33.648110Z","shell.execute_reply.started":"2025-04-29T12:56:33.362984Z","shell.execute_reply":"2025-04-29T12:56:33.647329Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# Count occurrences of severity levels per condition\nseverity_counts = train_data[condition_columns].apply(pd.Series.value_counts)\n\n# Reshape for plotting\nseverity_counts = severity_counts.T  # Transpose for better readability\nseverity_counts = severity_counts.reset_index().melt(id_vars='index', var_name='Severity Level', value_name='Count')\nseverity_counts.rename(columns={'index': 'Condition'}, inplace=True)\nseverity_counts.sort_values('Condition')\nseverity_order = [\"Normal/Mild\", \"Moderate\", \"Severe\"]\n\n# Convert the 'Severity Level' column to a categorical type with the defined order\nseverity_counts[\"Severity Level\"] = pd.Categorical(severity_counts[\"Severity Level\"], \n                                                   categories=severity_order, \n                                                   ordered=True)\n# Sort the dataframe based on this order\nseverity_counts = severity_counts.sort_values(by=[\"Severity Level\", \"Condition\"])\n\n\nplt.figure(figsize=(15,7))\n\n# Create grouped bar plot\nsns.barplot(data=severity_counts, x=\"Condition\", y=\"Count\", hue=\"Severity Level\", palette=\"Set1\")\n\n# Rotate x-axis labels for better readability\nplt.xticks(rotation=90, ha='right')\n# Add title and labels\nplt.title(\"Severity Level Distribution for Each Lumbar Spine Condition\")\nplt.xlabel(\"Condition\")\nplt.ylabel(\"Count\")\nplt.legend(title=\"Severity Level\")\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-04-29T12:56:33.650741Z","iopub.execute_input":"2025-04-29T12:56:33.650953Z","iopub.status.idle":"2025-04-29T12:56:34.591972Z","shell.execute_reply.started":"2025-04-29T12:56:33.650934Z","shell.execute_reply":"2025-04-29T12:56:34.591079Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# Easier alternative to plot the Severity Level Distribution\nfigure, axis = plt.subplots(1,3, figsize=(20,5))\nfor idx, condition in enumerate(['foraminal', 'subarticular', 'canal']):\n    diagnosis = list(filter(lambda x: x.find(condition)!= -1, train_data.columns))\n    filtered_dataframe_by_diagnosis = train_data[diagnosis]\n    value_counts = filtered_dataframe_by_diagnosis.apply(filtered_dataframe_by_diagnosis.value_counts).T\n    value_counts.plot(kind='bar',stacked=True, ax=axis[idx], cmap='Set1')","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-04-29T12:56:34.594051Z","iopub.execute_input":"2025-04-29T12:56:34.594280Z","iopub.status.idle":"2025-04-29T12:56:35.401401Z","shell.execute_reply.started":"2025-04-29T12:56:34.594260Z","shell.execute_reply":"2025-04-29T12:56:35.400580Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"grouped_counts = train_data[condition_columns]\n\n# Function to extract disease category (splitting by the second last '_')\ndef extract_disease_category(col_name):\n    parts = col_name.split('_')\n    if len(parts) > 2:\n        return ' '.join(parts[:-2]).title() \n    return col_name  \n\n# Create a mapping of condition columns to disease categories\ncolumn_category_mapping ={col: extract_disease_category(col) for col in condition_columns}\n\n# Group columns by disease category\ngrouped_severity_counts = {}\nfor category in column_category_mapping.values():\n    category_columns = [col for col in condition_columns if column_category_mapping[col] == category]\n    grouped_data = grouped_counts[category_columns].apply(pd.Series.value_counts).apply(pd.Series.sum, axis=1)\n    grouped_severity_counts[category] =  grouped_data\n \ngrouped_severity_counts_df = pd.DataFrame(grouped_severity_counts)\ngrouped_severity_counts_df = grouped_severity_counts_df.reset_index().melt(id_vars='index', var_name='Disease Category', value_name='Count')\ngrouped_severity_counts_df.rename(columns={'index': 'Severity Level'}, inplace=True)\n\ngrouped_severity_counts_df[\"Severity Level\"] = pd.Categorical(grouped_severity_counts_df[\"Severity Level\"], \n                                                   categories=severity_order, \n                                                   ordered=True)\n# Sort the dataframe based on this order\ngrouped_severity_counts_df = grouped_severity_counts_df.sort_values(by=[\"Severity Level\"])\n\n# Plot grouped dataframe\nsns.barplot(data=grouped_severity_counts_df, x=\"Disease Category\", y=\"Count\", hue=\"Severity Level\", palette=\"Set1\")\n\n# Rotate x-axis labels for better readability\nplt.xticks(rotation=45, ha='right')\n# Add title and labels\nplt.title(\"Severity Level Distribution for Each Lumbar Spine Condition\")\nplt.xlabel(\"Condition\")\nplt.ylabel(\"Count\")\nplt.legend(title=\"Severity Level\") ","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-04-29T12:56:35.402429Z","iopub.execute_input":"2025-04-29T12:56:35.402721Z","iopub.status.idle":"2025-04-29T12:56:35.862520Z","shell.execute_reply.started":"2025-04-29T12:56:35.402699Z","shell.execute_reply":"2025-04-29T12:56:35.861654Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# Create the count plot\ndisk_level_counts = train_label['level'].value_counts()\n\nsns.barplot(x=disk_level_counts.index, y=disk_level_counts.values, palette=\"Set1\")\nplt.xlabel(\"Disk Levels\")\nplt.ylabel(\"Count\")\nplt.title(\"Distribution of Disk Levels Across All Lumbar Spine Conditions\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-04-29T12:56:35.863264Z","iopub.execute_input":"2025-04-29T12:56:35.863546Z","iopub.status.idle":"2025-04-29T12:56:36.104170Z","shell.execute_reply.started":"2025-04-29T12:56:35.863526Z","shell.execute_reply":"2025-04-29T12:56:36.103317Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"def visualize_condition_counts(df, title, path=None):\n    \n    \"\"\"\n    Visualize condition counts on training set\n    \n    Parameters\n    ----------\n    df: pandas.DataFrame\n        Counts and percentages of conditions\n        \n    title: str\n        Title of the plot\n        \n    path: str, pathlib.Path or None\n        Path of the output file (if path is None, plot is displayed with selected backend)\n    \"\"\"\n    \n    fig, ax = plt.subplots(figsize=(10, 7))\n\n    ax.barh(\n        y=np.arange(df.shape[0] // 3) - 0.2,\n        width=df['count'].values[0::3],\n        height=0.2,\n        align='center',\n        label='Normal/Mild'\n    )\n    ax.barh(\n        y=np.arange(df.shape[0] // 3),\n        width=df['count'].values[1::3],\n        height=0.2,\n        align='center',\n        label='Moderate'\n    )\n    ax.barh(\n        y=np.arange(df.shape[0] // 3) + 0.2,\n        width=df['count'].values[2::3],\n        height=0.2,\n        align='center',\n        label='Severe'\n    )\n    ax.set_yticks(np.arange(df.shape[0] // 3))\n    ax.set_yticklabels([\n        f'{level}\\nNormal Count: {normal_count} ({normal_percentage:.2f}%)\\nModerate Count: {moderate_count} ({moderate_percentage:.2f}%)\\nSevere Count: {severe_count} ({severe_percentage:.2f}%)' for level, normal_count, normal_percentage, moderate_count, moderate_percentage, severe_count, severe_percentage, in zip(\n            df['Spine Level'].values[0::3],\n            df['count'].values[0::3],\n            df['percentage'].values[0::3],\n            df['count'].values[1::3],\n            df['percentage'].values[1::3],\n            df['count'].values[2::3],\n            df['percentage'].values[2::3],\n        )\n    ])\n    ax.set_xlabel('')\n    ax.tick_params(axis='x', pad=10)\n    ax.tick_params(axis='y')\n    ax.set_title(title, pad=15)\n    ax.legend(loc='best')\n    plt.gca().invert_yaxis()\n\n    plt.show()\n\n    if path is None:\n        plt.show()\n    else:\n        plt.savefig(path)\n        plt.close(fig)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-04-29T12:56:36.104941Z","iopub.execute_input":"2025-04-29T12:56:36.105207Z","iopub.status.idle":"2025-04-29T12:56:36.114103Z","shell.execute_reply.started":"2025-04-29T12:56:36.105187Z","shell.execute_reply":"2025-04-29T12:56:36.113365Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# Get unique disease categories\ndisease_categories = sorted(set(column_category_mapping.values()))\n\n# Dictionary to store processed severity data\nall_conditions_data = {}\n\nfor category in disease_categories:\n    # Select columns belonging to this disease category\n    category_columns = [col for col in condition_columns if column_category_mapping[col] == category]\n\n    # Create a DataFrame with severity levels mapped to spine levels\n    df_condition = train_data[category_columns].copy()\n\n    # Convert to long format (stacked)\n    df_condition = df_condition.stack().reset_index()\n   \n    df_condition = df_condition.rename(columns={0: 'Severity', 'level_1': 'Spine Level'})\n    # Extract spine level (last two parts of column name)\n    df_condition['Spine Level'] = df_condition['Spine Level'].apply(lambda x: ' '.join(x.split('_')[-2:]).title())\n\n    # Count occurrences\n    df_condition_counts = df_condition.groupby(['Spine Level', 'Severity']).size().reset_index(name='count')\n\n    # Map severity to numerical values\n    severity_map = {'Normal/Mild': 0, 'Moderate': 1, 'Severe': 2}\n    df_condition_counts['Severity Level'] = df_condition_counts['Severity'].map(severity_map)\n    \n    # Sort by Spine Level and Severity Level\n    df_condition_counts = df_condition_counts.sort_values(by=['Spine Level', 'Severity Level'], ascending=True)\n\n    # Compute percentages per level\n    df_condition_counts['percentage'] = df_condition_counts['count'] / df_condition_counts.groupby('Spine Level')['count'].transform('sum') * 100\n\n    # Store processed data\n    all_conditions_data[category] = df_condition_counts\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-04-29T12:56:36.114891Z","iopub.execute_input":"2025-04-29T12:56:36.115161Z","iopub.status.idle":"2025-04-29T12:56:36.207120Z","shell.execute_reply.started":"2025-04-29T12:56:36.115142Z","shell.execute_reply":"2025-04-29T12:56:36.206538Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# Visualize each condition separately\nfor category, df_counts in all_conditions_data.items():\n    visualize_condition_counts(\n        df=df_counts,\n        title=f\"{category} Counts By Spine Level\"\n    )\n\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-04-29T12:56:36.207950Z","iopub.execute_input":"2025-04-29T12:56:36.208240Z","iopub.status.idle":"2025-04-29T12:56:37.572784Z","shell.execute_reply.started":"2025-04-29T12:56:36.208205Z","shell.execute_reply":"2025-04-29T12:56:37.571849Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## Data Visualization","metadata":{}},{"cell_type":"code","source":"def generate_image_paths(data_frame, data_dir):\n    image_paths = []\n    for study_id, series_id in zip(data_frame['study_id'], data_frame['series_id']):\n        study_dir = os.path.join(data_dir, str(study_id))\n        series_dir = os.path.join(study_dir, str(series_id))\n        image_paths.extend([os.path.join(series_dir, img) for img in os.listdir(series_dir)])\n    return image_paths\n\ntrain_image_paths = generate_image_paths(train_description, os.path.join(data_path, \"train_images\"))","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-04-29T12:56:37.573746Z","iopub.execute_input":"2025-04-29T12:56:37.574030Z","iopub.status.idle":"2025-04-29T12:58:15.787069Z","shell.execute_reply.started":"2025-04-29T12:56:37.574007Z","shell.execute_reply":"2025-04-29T12:58:15.786389Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"def display_dicom_images(image_paths):\n    plt.figure(figsize=(15, 5))  # Adjust figure size if needed\n    for i, path in enumerate(image_paths[:3]):\n        ds = pydicom.dcmread(path)\n        plt.subplot(1, 3, i+1)\n        plt.imshow(ds.pixel_array, cmap=plt.cm.bone)\n        plt.title(f\"Image {i+1}\")\n        plt.axis('off')\n    plt.show()\ndisplay_dicom_images(train_image_paths)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-04-29T12:58:15.787920Z","iopub.execute_input":"2025-04-29T12:58:15.788211Z","iopub.status.idle":"2025-04-29T12:58:16.234279Z","shell.execute_reply.started":"2025-04-29T12:58:15.788182Z","shell.execute_reply":"2025-04-29T12:58:16.233417Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"def load_dicom_files(path_to_folder):\n    # Filter and sort DICOM files based on numeric part extracted from filename.\n    dicom_filenames = sorted(\n        [f for f in os.listdir(path_to_folder) if f.endswith('.dcm')],\n        key=lambda f: int(os.path.splitext(f)[0].split('-')[-1])\n    )\n    return [os.path.join(path_to_folder, f) for f in dicom_filenames]\n\ndef load_first_dicom(series_folder_path):\n    dicom_files = load_dicom_files(series_folder_path)\n    return dicom_files[0] if dicom_files else None","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-04-29T12:58:16.235440Z","iopub.execute_input":"2025-04-29T12:58:16.235795Z","iopub.status.idle":"2025-04-29T12:58:16.241556Z","shell.execute_reply.started":"2025-04-29T12:58:16.235758Z","shell.execute_reply":"2025-04-29T12:58:16.240609Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# ---------------------------\n# Helper Functions\n# ---------------------------\n\ndef iou(box1, box2):\n    \"\"\"\n    Compute Intersection over Union (IoU) between two boxes.\n    Each box is defined as a tuple: (x1, y1, x2, y2)\n    \"\"\"\n    x1 = max(box1[0], box2[0])\n    y1 = max(box1[1], box2[1])\n    x2 = min(box1[2], box2[2])\n    y2 = min(box1[3], box2[3])\n    inter_area = max(0, x2 - x1) * max(0, y2 - y1)\n    box1_area = (box1[2] - box1[0]) * (box1[3] - box1[1])\n    box2_area = (box2[2] - box2[0]) * (box2[3] - box2[1])\n    union_area = box1_area + box2_area - inter_area\n    return inter_area / union_area if union_area != 0 else 0\n\ndef cluster_boxes_representative(centers, box_size, iou_threshold=0.3):\n    \"\"\"\n    Cluster bounding boxes (given by their center coordinates) that are close together.\n    Instead of averaging, choose one representative center (the first in the cluster).\n    \n    Parameters:\n      centers: list of tuples (center_x, center_y)\n      box_size: tuple (width, height) for the boxes\n      iou_threshold: threshold for clustering boxes\n    \n    Returns:\n      A list of representative center coordinates (from the original annotations).\n    \"\"\"\n    if not centers:\n        return []\n    \n    w, h = box_size\n    # Create boxes for each center.\n    boxes = [(cx - w/2, cy - h/2, cx + w/2, cy + h/2) for cx, cy in centers]\n    \n    clusters = []  # each cluster is a list of indices\n    used = set()\n    \n    for i in range(len(boxes)):\n        if i in used:\n            continue\n        cluster = [i]\n        used.add(i)\n        for j in range(i+1, len(boxes)):\n            if j in used:\n                continue\n            if iou(boxes[i], boxes[j]) >= iou_threshold:\n                cluster.append(j)\n                used.add(j)\n        clusters.append(cluster)\n    \n    # Choose the first center from each cluster as the representative.\n    representative_centers = [centers[cluster[0]] for cluster in clusters]\n    return representative_centers\n\n# ---------------------------\n# Display Functions\n# ---------------------------\n\ndef display_dicom_for_condition(image, study_id, series_id, cond, rows, box_size=(50,50), iou_threshold=0.3, color='red'):\n    \"\"\"\n    Plot a single image with bounding boxes for a given condition.\n    Uses the representative annotation from each cluster.\n    \"\"\"\n    # Extract centers for the current condition and cluster them.\n    centers = [(row['x'], row['y']) for row in rows]\n    rep_centers = cluster_boxes_representative(centers, box_size, iou_threshold=iou_threshold)\n    \n    fig, ax = plt.subplots(figsize=(6,6))\n    ax.imshow(image, cmap='gray')\n    ax.set_title(f\"Study: {study_id}, Series: {series_id} - {cond}\")\n    ax.axis('off')\n    \n    for center_x, center_y in rep_centers:\n        # Compute top-left of the bounding box from the representative center.\n        top_left = (center_x - box_size[0]/2, center_y - box_size[1]/2)\n        rect = patches.Rectangle(top_left, box_size[0], box_size[1],\n                                 linewidth=2, edgecolor=color, facecolor='none')\n        ax.add_patch(rect)\n        ax.text(center_x, top_left[1] - 5, cond, color=color, fontsize=8, ha='center')\n    \n    plt.tight_layout()\n    plt.show()\n\ndef display_dicom_with_conditions(image_paths, label_df, box_size=(50,50), iou_threshold=0.3):\n    \"\"\"\n    For each image, if multiple conditions are present, duplicate the image and plot one condition per subplot.\n    \"\"\"\n    # Build mapping from (study_id, series_id) to annotations.\n    label_dict = {}\n    for _, row in label_df.iterrows():\n        key = (int(row['study_id']), int(row['series_id']))\n        label_dict.setdefault(key, []).append(row)\n    \n    # Define colors for conditions.\n    available_colors = ['red', 'blue', 'green', 'orange', 'purple', 'cyan', 'magenta', 'yellow']\n    condition_colors = {}\n    for cond in label_df['condition'].unique():\n        condition_colors[cond] = available_colors[len(condition_colors) % len(available_colors)]\n    \n    def read_dicom(path):\n        ds = pydicom.dcmread(path)\n        return ds.pixel_array\n\n    # Load DICOM images concurrently.\n    with ThreadPoolExecutor(max_workers=4) as executor:\n        future_to_path = {executor.submit(read_dicom, path): path for path in image_paths}\n        images = {}\n        for future in future_to_path:\n            path = future_to_path[future]\n            try:\n                images[path] = future.result()\n            except Exception as exc:\n                print(f\"Error reading {path}: {exc}\")\n                images[path] = None\n\n    # Process each image.\n    for path in image_paths:\n        parts = path.split(os.sep)\n        try:\n            study_id = int(parts[-3])\n            series_id = int(parts[-2])\n        except (IndexError, ValueError):\n            print(f\"Unable to parse IDs from path: {path}\")\n            continue\n        \n        image = images.get(path)\n        if image is None:\n            continue\n        \n        # Get all annotations for this image.\n        annotations = label_dict.get((study_id, series_id), [])\n        if annotations:\n            # Group annotations by condition.\n            condition_groups = {}\n            for row in annotations:\n                cond = row['condition']\n                condition_groups.setdefault(cond, []).append(row)\n            \n            # For each condition, duplicate the image and plot only that condition.\n            for cond, rows in condition_groups.items():\n                color = condition_colors.get(cond, 'red')\n                display_dicom_for_condition(image, study_id, series_id, cond, rows,\n                                            box_size=box_size, iou_threshold=iou_threshold, color=color)\n        else:\n            # If no annotations, just show the image.\n            fig, ax = plt.subplots(figsize=(6,6))\n            ax.imshow(image, cmap='gray')\n            ax.set_title(f\"Study: {study_id}, Series: {series_id}\")\n            ax.axis('off')\n            plt.tight_layout()\n            plt.show()\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-04-29T12:58:16.242578Z","iopub.execute_input":"2025-04-29T12:58:16.242858Z","iopub.status.idle":"2025-04-29T12:58:16.261715Z","shell.execute_reply.started":"2025-04-29T12:58:16.242827Z","shell.execute_reply":"2025-04-29T12:58:16.260881Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"train_images_dir = os.path.join(data_path, 'train_images')\nstudy_ids = [folder for folder in os.listdir(train_images_dir) if os.path.isdir(os.path.join(train_images_dir, folder))]\nselected_study_id = random.choice(study_ids)\nprint(f\"Selected study_id: {selected_study_id}\")\n\n# Build study folder path for the selected study.\nstudy_folder = os.path.join(train_images_dir, selected_study_id)\n\n# List all series folders within the selected study folder.\nseries_folders = [os.path.join(study_folder, sf) for sf in os.listdir(study_folder)\n                  if os.path.isdir(os.path.join(study_folder, sf))]\n\n# Use ThreadPoolExecutor to get the first DICOM file per series in parallel.\nwith ThreadPoolExecutor(max_workers=4) as executor:\n    first_dicom_futures = [executor.submit(load_first_dicom, series_folder) for series_folder in series_folders]\n    image_paths = [f.result() for f in first_dicom_futures if f.result() is not None]\n\n# Now display the images with separate plots per condition.\ndisplay_dicom_with_conditions(image_paths, train_label, box_size=(25,25), iou_threshold=0.3)\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-04-29T12:58:16.262604Z","iopub.execute_input":"2025-04-29T12:58:16.262865Z","iopub.status.idle":"2025-04-29T12:58:21.338306Z","shell.execute_reply.started":"2025-04-29T12:58:16.262840Z","shell.execute_reply":"2025-04-29T12:58:21.337457Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## Training of YOLO Model","metadata":{}},{"cell_type":"code","source":"# Merge the two dataframes on 'study_id' and 'series_id'\nmerged_csv = pd.merge(train_label, train_description[['study_id', 'series_id', 'series_description']], \n                      on=['study_id', 'series_id'], how='left')\nmerged_csv","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-04-29T12:58:21.339406Z","iopub.execute_input":"2025-04-29T12:58:21.339715Z","iopub.status.idle":"2025-04-29T12:58:21.368058Z","shell.execute_reply.started":"2025-04-29T12:58:21.339686Z","shell.execute_reply":"2025-04-29T12:58:21.367378Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# We want to obtain three different csv files for each condition\ncleaned_data = merged_csv.copy()\n# Define conditions for each group\ncondition_groups = {\n    'Spinal Canal Stenosis': ['Spinal Canal Stenosis'],\n    'Neural Foraminal Narrowing': ['Right Neural Foraminal Narrowing', 'Left Neural Foraminal Narrowing'],\n    'Subarticular Stenosis': ['Right Subarticular Stenosis', 'Left Subarticular Stenosis']\n}\n\n# Split and save to separate CSV files\nfor group_name, conditions in condition_groups.items():\n    # Filter rows based on condition\n    filtered_df = cleaned_data[cleaned_data['condition'].isin(conditions)]\n    # Save to new CSV file\n    group_name_save=group_name.replace(' ','_')\n    filtered_df.to_csv(f'{group_name_save}.csv', index=False)\n\nprint(\"CSV files have been split and saved.\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-04-29T12:58:21.368854Z","iopub.execute_input":"2025-04-29T12:58:21.369148Z","iopub.status.idle":"2025-04-29T12:58:21.601103Z","shell.execute_reply.started":"2025-04-29T12:58:21.369116Z","shell.execute_reply":"2025-04-29T12:58:21.600239Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# -----------------------------------------------------------\n# 1. Helper function to perform an 80/20 stratified split\n# -----------------------------------------------------------\ndef train_val_split(csv_file, results_name):\n    # Load CSV\n    df = pd.read_csv(csv_file)\n    \n    # Create a unique label from condition and level and assign numeric codes\n    df['condition_level'] = df['condition'] + '_' + df['level']\n    df['class_id'] = df['condition_level'].astype('category').cat.codes\n\n    # Perform an 80/20 split (stratified by class_id)\n    train_df, val_df = train_test_split(df, test_size=0.2, stratify=df['class_id'], random_state=42)\n    \n    # Save the splits (if desired)\n    train_df.to_csv(f'{results_name}_train.csv', index=False)\n    val_df.to_csv(f'{results_name}_val.csv', index=False)\n    \n    print(f\"Data split: train ({len(train_df)}) and val ({len(val_df)})\")\n    return train_df, val_df\n\n# -----------------------------------------------------------\n# 2. Updated Data Preparation Class for ML pipelines\n# -----------------------------------------------------------\nclass DetectorDataPreparation:\n    def __init__(\n        self,\n        dataset_directory='../Data/train_images',\n        csv_directory='',\n        condition_level_classes={},\n        condition_name='',\n        width_box=16\n    ):\n        self.dataset_directory = dataset_directory\n        self.csv_directory = csv_directory\n        self.condition_level_classes = condition_level_classes\n        self.condition_name = condition_name\n        self.width_box = width_box\n        \n        # Define a directory to save processed data\n        self.save_directory = f'./{self.condition_name}'\n        \n        # Create folder structure\n        self.create_folder()\n        \n        # Read CSV and perform an 80/20 train–validation split\n        self.read_train_val_split()\n        \n        # Process training data: convert DICOM to PNG, save HW info, merge, then create YOLO labels\n        self.dicom_to_png(self.training_data, self.train_image_path)\n        self.training_data = self.save_height_width_to_csv(self.training_data, data_type='train')\n        self.create_label_for_yolo(self.training_data, self.train_labels_path)\n        \n        # Process validation data\n        self.dicom_to_png(self.validation_data, self.val_images_path)\n        self.validation_data = self.save_height_width_to_csv(self.validation_data, data_type='val')\n        self.create_label_for_yolo(self.validation_data, self.val_labels_path)\n        \n        # Create a YAML config file for YOLO training\n        self.create_yaml_file()\n\n    def create_folder(self):\n        # Base folder\n        base_path = Path(self.save_directory)\n        base_path.mkdir(parents=True, exist_ok=True)\n        \n        # Dataset folder and subfolders for training and validation (images and labels)\n        self.dataset_path = base_path / 'datasets'\n        (self.dataset_path / 'train/images').mkdir(parents=True, exist_ok=True)\n        (self.dataset_path / 'train/labels').mkdir(parents=True, exist_ok=True)\n        (self.dataset_path / 'val/images').mkdir(parents=True, exist_ok=True)\n        (self.dataset_path / 'val/labels').mkdir(parents=True, exist_ok=True)\n        \n        self.train_image_path = self.dataset_path / 'train/images'\n        self.train_labels_path = self.dataset_path / 'train/labels'\n        self.val_images_path = self.dataset_path / 'val/images'\n        self.val_labels_path = self.dataset_path / 'val/labels'\n    \n    def read_train_val_split(self):\n        df = pd.read_csv(self.csv_directory)\n        # Create a unique class from condition and level\n        df['condition_level'] = df['condition'] + '_' + df['level']\n        df['class_id'] = df['condition_level'].astype('category').cat.codes\n\n        # 80/20 stratified split based on the numeric class labels\n        train_df, val_df = train_test_split(df, test_size=0.2, stratify=df['class_id'], random_state=42)\n        train_df['split'] = 'train'\n        val_df['split'] = 'val'\n        \n        self.training_data = train_df.copy()\n        self.validation_data = val_df.copy()\n        \n        # Save split files (optional)\n        train_df.to_csv(f'{self.save_directory}/train_split.csv', index=False)\n        val_df.to_csv(f'{self.save_directory}/val_split.csv', index=False)\n        print(f\"Training samples: {len(train_df)}, Validation samples: {len(val_df)}\")\n    \n    def read_dicom(self, dicom_path):\n        # Read a DICOM file and convert to a 3-channel image (normalized to 0-255)\n        ds = pydicom.dcmread(dicom_path)\n        image = ds.pixel_array.astype(np.float32)\n        image = (image - image.min()) / (image.max() - image.min() + 1e-6) * 255\n        image = np.stack([image] * 3, axis=-1).astype('uint8')\n        return image\n    \n    def dicom_to_png(self, df, image_directory):\n        # Reset the list for height and width info for each split\n        self.height_width_info = []\n        for _, row in df.iterrows():\n            study_id = row['study_id']\n            series_id = row['series_id']\n            instance = row['instance_number']\n            dcm_path = f'{self.dataset_directory}/{study_id}/{series_id}/{instance}.dcm'\n            try:\n                dcm_image = self.read_dicom(dcm_path)\n            except Exception as e:\n                print(f\"Error reading DICOM {dcm_path}: {e}\")\n                continue\n            height, width, _ = dcm_image.shape\n            self.height_width_info.append({\n                'study_id': study_id,\n                'series_id': series_id,\n                'instance_number': instance,\n                'height': height,\n                'width': width\n            })\n            png_filename = f'{study_id}_{series_id}_{instance}.png'\n            cv2.imwrite(str(Path(image_directory) / png_filename), dcm_image)\n    \n    def save_height_width_to_csv(self, df, data_type='train'):\n        # Save the height/width info extracted during DICOM conversion\n        csv_file = f'{self.save_directory}/{self.condition_name}_height_width_{data_type}.csv'\n        with open(csv_file, mode='w', newline='') as file:\n            writer = csv.DictWriter(file, fieldnames=self.height_width_info[0].keys())\n            writer.writeheader()\n            writer.writerows(self.height_width_info)\n        print(f\"Height/width data saved to {csv_file}\")\n        \n        # Merge the height/width info with the original dataframe on study_id, series_id, and instance_number\n        hw_df = pd.read_csv(csv_file)\n        merged_data = pd.merge(df, hw_df, on=['study_id', 'series_id', 'instance_number'], how='left')\n        merged_csv = f'{self.save_directory}/{self.condition_name}_merged_{data_type}.csv'\n        merged_data.to_csv(merged_csv, index=False)\n        print(f\"Merged data saved to {merged_csv}\")\n        \n        # Return the merged DataFrame for later use\n        return merged_data\n    \n    def find_class_label(self, condition, level):\n        # Construct the key exactly as defined in condition_level_classes\n        key = f\"{condition.replace(' ', '_')}_{level.replace('/', '_')}\"\n        return self.condition_level_classes.get(key, -1)\n    \n    def create_label_for_yolo(self, df, labels_directory):\n        # For each row (i.e. each instance), write a YOLO label file\n        for _, row in df.iterrows():\n            # Ensure that the merged data has height/width information\n            if 'height' not in row or 'width' not in row:\n                print(f\"Missing height/width for row: {row}\")\n                continue\n            study_id = row['study_id']\n            series_id = row['series_id']\n            instance = row['instance_number']\n            height = row['height']\n            width = row['width']\n            condition = row['condition']\n            level = row['level']\n            # Expecting x and y columns to be present in your CSV\n            x = row['x']\n            y = row['y']\n            class_id = self.find_class_label(condition, level)\n            \n            # Normalize the x, y, width and height for YOLO (assuming width_box defines a fixed box size)\n            norm_x = float(x) / width\n            norm_y = float(y) / height\n            norm_w = float(self.width_box) / width\n            norm_h = float(self.width_box) / height\n            \n            label_filename = f'{study_id}_{series_id}_{instance}.txt'\n            with open(str(Path(labels_directory) / label_filename), 'w') as f:\n                f.write(f\"{class_id} {norm_x} {norm_y} {norm_w} {norm_h}\\n\")\n    \n    def create_yaml_file(self):\n        # Create a YAML file for YOLO that contains paths, number of classes, and class names\n        yaml_file_path = f'{self.dataset_path}/yolo_config.yaml'\n        num_classes = len(self.condition_level_classes)\n        yaml_data = {\n            'train': './train',\n            'val': './val',\n            'nc': num_classes,\n            'names': list(self.condition_level_classes.keys())\n        }\n        with open(yaml_file_path, 'w') as file:\n            yaml.dump(yaml_data, file, default_flow_style=False)\n        print(f\"YAML config saved to {yaml_file_path}\")\n\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-04-29T12:58:21.602121Z","iopub.execute_input":"2025-04-29T12:58:21.602421Z","iopub.status.idle":"2025-04-29T12:58:21.624107Z","shell.execute_reply.started":"2025-04-29T12:58:21.602398Z","shell.execute_reply":"2025-04-29T12:58:21.623420Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"condition_level_classes_spinal_canal = {\n    'Spinal_Canal_Stenosis_L1_L2': 0,\n    'Spinal_Canal_Stenosis_L2_L3': 1,\n    'Spinal_Canal_Stenosis_L3_L4': 2,\n    'Spinal_Canal_Stenosis_L4_L5': 3,\n    'Spinal_Canal_Stenosis_L5_S1': 4,\n}\n\ndataset_dir = '/kaggle/input/rsna-2024-lumbar-spine-degenerative-classification/train_images'\ncsv_path = './Spinal_Canal_Stenosis.csv' \n\n# Instantiate the data preparation class.\nprep = DetectorDataPreparation(\n    dataset_directory=dataset_dir,\n    csv_directory=csv_path,\n    condition_level_classes=condition_level_classes_spinal_canal,\n    condition_name='Spinal_Canal_Stenosis',\n    width_box=16\n)\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-04-29T12:58:21.624901Z","iopub.execute_input":"2025-04-29T12:58:21.625154Z","iopub.status.idle":"2025-04-29T13:02:18.851712Z","shell.execute_reply.started":"2025-04-29T12:58:21.625122Z","shell.execute_reply":"2025-04-29T13:02:18.850732Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"!pip install ultralytics","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-04-29T13:02:18.852807Z","iopub.execute_input":"2025-04-29T13:02:18.853142Z","iopub.status.idle":"2025-04-29T13:02:24.921404Z","shell.execute_reply.started":"2025-04-29T13:02:18.853109Z","shell.execute_reply":"2025-04-29T13:02:24.920327Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"from ultralytics import YOLO  # ✅ required import","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-04-29T13:02:24.922574Z","iopub.execute_input":"2025-04-29T13:02:24.922838Z","iopub.status.idle":"2025-04-29T13:02:25.036135Z","shell.execute_reply.started":"2025-04-29T13:02:24.922813Z","shell.execute_reply":"2025-04-29T13:02:25.035527Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"class YOLOTraining:\n    def __init__(self,\n                 data_directory,\n                 condition,\n                 results_directory,\n                 epochs=500,\n                 patience=20,\n                 batch=4):\n        self.condition = condition\n        self.results_directory = results_directory\n        self.result_condition_directory = f'{self.results_directory}/{self.condition}'\n        self.data_directory = data_directory\n        self.epochs = epochs\n        self.patience = patience\n        self.batch = batch\n\n        # The YAML configuration is now under the datasets folder (80/20 split)\n        self.yolo_config_yaml = f'{self.data_directory}/{self.condition}/datasets/yolo_config.yaml'\n        # Load a pretrained YOLO model\n        model = self.load_pretrain_model()\n\n        # Start training\n        self.results = self.training(model)\n\n    def load_pretrain_model(self):\n        # Build a new model from the model configuration YAML and load pretrained weights\n        # model = YOLO('yolov8n.yaml')  # build a new model from YAML\n        model = YOLO('yolo11n.pt')    # load pretrained weights\n        return model\n\n    def training(self, model):\n        # Ensure the results folder exists\n        result_condition_path = Path(self.result_condition_directory)\n        if not result_condition_path.exists():\n            result_condition_path.mkdir(parents=True, exist_ok=True)\n\n        # Define a name for this training run\n        name = f\"epochs-{self.epochs}_batch-{self.batch}_{datetime.datetime.now().strftime('%Y%m%d_%H%M%S')}\"\n\n\n        # Train the model using the YAML configuration for the 80/20 split data\n        results = model.train(data=self.yolo_config_yaml,\n                              project=result_condition_path,\n                              name=name,\n                              epochs=self.epochs,\n                              patience=self.patience,\n                              batch=self.batch)\n        print(\"Training complete!\")\n        return results\n\n\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-04-29T14:15:20.459148Z","iopub.execute_input":"2025-04-29T14:15:20.459508Z","iopub.status.idle":"2025-04-29T14:15:20.466069Z","shell.execute_reply.started":"2025-04-29T14:15:20.459471Z","shell.execute_reply":"2025-04-29T14:15:20.465100Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# Train Yolo Model for each condition separately\ncondition = 'Spinal_Canal_Stenosis'       \nresults_directory = './results_spinal_canal_stenosis'          \nepochs = 1\npatience = 20\nbatch = 4\n\n\n# Instantiate the YOLO training class with the provided arguments\nresult = YOLOTraining(data_directory=\" /kaggle/working\",\n             condition=condition,\n             results_directory=results_directory,\n             epochs=epochs,\n             patience=patience,\n             batch=batch)\n\n\n# Access specific metrics\nmetrics = result.metrics  # .metrics is a dict!\n\nprint(metrics)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-04-29T14:15:23.663164Z","iopub.execute_input":"2025-04-29T14:15:23.663521Z","iopub.status.idle":"2025-04-29T14:17:01.466680Z","shell.execute_reply.started":"2025-04-29T14:15:23.663491Z","shell.execute_reply":"2025-04-29T14:17:01.465155Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"metrics = result.results  # .metrics is a dict!\n\nprint(metrics)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-04-29T14:21:24.094796Z","iopub.execute_input":"2025-04-29T14:21:24.095141Z","iopub.status.idle":"2025-04-29T14:21:24.112402Z","shell.execute_reply.started":"2025-04-29T14:21:24.095116Z","shell.execute_reply":"2025-04-29T14:21:24.111061Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"print(result.yolo_config_yaml)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-04-29T14:07:53.533578Z","iopub.execute_input":"2025-04-29T14:07:53.533881Z","iopub.status.idle":"2025-04-29T14:07:53.538581Z","shell.execute_reply.started":"2025-04-29T14:07:53.533859Z","shell.execute_reply":"2025-04-29T14:07:53.537628Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# Subarticular Stenosis\ncondition_level_classes_subarticular = {\n    'Left_Subarticular_Stenosis_L1_L2': 0,\n    'Left_Subarticular_Stenosis_L2_L3': 1,\n    'Left_Subarticular_Stenosis_L3_L4': 2,\n    'Left_Subarticular_Stenosis_L4_L5': 3,\n    'Left_Subarticular_Stenosis_L5_S1': 4,\n    'Right_Subarticular_Stenosis_L1_L2': 5,\n    'Right_Subarticular_Stenosis_L2_L3': 6,\n    'Right_Subarticular_Stenosis_L3_L4': 7,\n    'Right_Subarticular_Stenosis_L4_L5': 8,\n    'Right_Subarticular_Stenosis_L5_S1': 9,\n}\ncsv_path = './Subarticular_Stenosis.csv' \n\nDetectorDataPreparation(\n    dataset_directory=dataset_dir,\n    csv_directory=csv_path,\n    condition_level_classes=condition_level_classes_spinal_canal,\n    condition_name='Subarticular_Stenosis',\n    width_box=16\n)\n\n\ncondition = 'Subarticular_Stenosis'       \nresults_directory = './results_subarticular_stenosis'          \nepochs = 1\npatience = 20\nbatch = 4\n\nresults = YOLOTraining(data_directory=\" /kaggle/working\",\n             condition=condition,\n             results_directory=results_directory,\n             epochs=epochs,\n             patience=patience,\n             batch=batch)\n\n# 'results' now holds the metrics!\nprint(results)  # Will show basic info\n\n# Access specific metrics\nmetrics = results.metrics  # .metrics is a dict!\n\nprint(\"mAP50-95:\", metrics['mAP50-95'])\nprint(\"mAP50:\", metrics['mAP50'])\nprint(\"Precision:\", metrics['precision'])\nprint(\"Recall:\", metrics['recall'])\nprint('Subarticular Stenosis DONE')\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-04-29T13:02:25.503542Z","iopub.status.idle":"2025-04-29T13:02:25.503930Z","shell.execute_reply":"2025-04-29T13:02:25.503757Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"!git clone https://github.com/ultralytics/yolov5","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-04-29T14:05:57.623257Z","iopub.execute_input":"2025-04-29T14:05:57.623619Z","iopub.status.idle":"2025-04-29T14:05:59.502847Z","shell.execute_reply.started":"2025-04-29T14:05:57.623591Z","shell.execute_reply":"2025-04-29T14:05:59.501954Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"%cd ..\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-04-29T14:11:59.390771Z","iopub.execute_input":"2025-04-29T14:11:59.391116Z","iopub.status.idle":"2025-04-29T14:11:59.397391Z","shell.execute_reply.started":"2025-04-29T14:11:59.391089Z","shell.execute_reply":"2025-04-29T14:11:59.396503Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"!ls ","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-04-29T14:14:39.486097Z","iopub.execute_input":"2025-04-29T14:14:39.486347Z","iopub.status.idle":"2025-04-29T14:14:39.657415Z","shell.execute_reply.started":"2025-04-29T14:14:39.486309Z","shell.execute_reply":"2025-04-29T14:14:39.656257Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import os\nos.environ['WANDB_DISABLED'] = 'true'","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-04-29T14:11:32.195763Z","iopub.execute_input":"2025-04-29T14:11:32.196118Z","iopub.status.idle":"2025-04-29T14:11:32.200491Z","shell.execute_reply.started":"2025-04-29T14:11:32.196090Z","shell.execute_reply":"2025-04-29T14:11:32.199579Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"!python yolov5/train.py --img 640 --batch 16 --epochs 50 --data Spinal_Canal_Stenosis/datasets/yolo_config.yaml --weights yolov5s.pt --name lumbar_yolov5\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-04-29T14:13:59.633307Z","iopub.execute_input":"2025-04-29T14:13:59.633792Z","iopub.status.idle":"2025-04-29T14:14:39.304907Z","shell.execute_reply.started":"2025-04-29T14:13:59.633760Z","shell.execute_reply":"2025-04-29T14:14:39.303994Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# Neural Foraminal Narrowing\ncondition_level_classes_neural = {\n    'Left_Neural_Foraminal_Narrowing_L1_L2': 0,\n    'Left_Neural_Foraminal_Narrowing_L2_L3': 1,\n    'Left_Neural_Foraminal_Narrowing_L3_L4': 2,\n    'Left_Neural_Foraminal_Narrowing_L4_L5': 3,\n    'Left_Neural_Foraminal_Narrowing_L5_S1': 4,\n    'Right_Neural_Foraminal_Narrowing_L1_L2': 5,\n    'Right_Neural_Foraminal_Narrowing_L2_L3': 6,\n    'Right_Neural_Foraminal_Narrowing_L3_L4': 7,\n    'Right_Neural_Foraminal_Narrowing_L4_L5': 8,\n    'Right_Neural_Foraminal_Narrowing_L5_S1': 9,\n}\n\nDetectorDataPreparation(\n    dataset_directory=dataset_dir,\n    csv_directory=csv_path,\n    condition_level_classes=condition_level_classes_spinal_canal,\n    condition_name='Neural_Foraminal_Narrowing',\n    width_box=16\n)\n\n\ncondition = 'Neural_Foraminal_Narrowing'       \nresults_directory = './results_neural_foraminal_narrowing.csv'          \nepochs = 1\npatience = 20\nbatch = 4\n\nYOLOTraining(data_directory=\" /kaggle/working\",\n             condition=condition,\n             results_directory=results_directory,\n             epochs=epochs,\n             patience=patience,\n             batch=batch)\n\nprint('Neural Foraminal Narrowing Stenosis DONE')\n\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-04-29T13:02:25.504934Z","iopub.status.idle":"2025-04-29T13:02:25.505318Z","shell.execute_reply":"2025-04-29T13:02:25.505148Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# 📌 1. IMPORTS\nimport torch\nimport torchvision\nfrom torchvision.models.detection.faster_rcnn import FastRCNNPredictor\nfrom torchvision.transforms import functional as F\nimport pandas as pd\nimport numpy as np\nimport os\nfrom PIL import Image\nfrom torch.utils.data import DataLoader, Dataset\nimport matplotlib.pyplot as plt\n\n# 📌 2. DATASET CLASS\nclass LumbarSpineDataset(Dataset):\n    def __init__(self, images_dir, labels_csv, transforms=None):\n        self.images_dir = images_dir\n        self.transforms = transforms\n        \n        self.labels = pd.read_csv(labels_csv)\n        self.image_ids = self.labels['image_id'].unique()\n        \n        # Group labels by image for fast access\n        self.image_boxes = self.labels.groupby('image_id')\n    \n    def __getitem__(self, idx):\n        image_id = self.image_ids[idx]\n        img_path = os.path.join(self.images_dir, f\"{image_id}.png\")  # Assuming .png format\n        img = Image.open(img_path).convert(\"RGB\")\n        \n        records = self.image_boxes.get_group(image_id)\n        \n        boxes = records[['x_min', 'y_min', 'x_max', 'y_max']].values\n        boxes = torch.as_tensor(boxes, dtype=torch.float32)\n        \n        labels = torch.ones((records.shape[0],), dtype=torch.int64)  # Assuming one class (e.g., \"spine\")\n        \n        target = {}\n        target['boxes'] = boxes\n        target['labels'] = labels\n        target['image_id'] = torch.tensor([idx])\n\n        if self.transforms:\n            img = self.transforms(img)\n        \n        else:\n            img = F.to_tensor(img)\n\n        return img, target\n\n    def __len__(self):\n        return len(self.image_ids)\n\n# 📌 3. COLLATE FUNCTION\ndef collate_fn(batch):\n    return tuple(zip(*batch))\n\n# 📌 4. MODEL SETUP\ndef get_model(num_classes):\n    # Load Faster R-CNN pre-trained on COCO\n    model = torchvision.models.detection.fasterrcnn_resnet50_fpn(weights=\"DEFAULT\")\n    \n    # Get number of input features for classifier\n    in_features = model.roi_heads.box_predictor.cls_score.in_features\n    \n    # Replace the head with a new one\n    model.roi_heads.box_predictor = FastRCNNPredictor(in_features, num_classes)\n    \n    return model\n\n# 📌 5. HYPERPARAMETERS\nBATCH_SIZE = 4\nNUM_CLASSES = 2  # 1 class (spine) + background\nNUM_EPOCHS = 10\nLEARNING_RATE = 0.005\n\n# 📌 6. PREPARE DATA\ntrain_dataset = LumbarSpineDataset(\n    images_dir='/kaggle/working',  # <<--- Change this!\n    labels_csv='/path/to/train_label_coordinates.csv'\n)\n\ntrain_loader = DataLoader(\n    train_dataset,\n    batch_size=BATCH_SIZE,\n    shuffle=True,\n    num_workers=2,\n    collate_fn=collate_fn\n)\n\n# 📌 7. PREPARE MODEL\ndevice = torch.device('cuda') if torch.cuda.is_available() else torch.device('cpu')\nmodel = get_model(NUM_CLASSES)\nmodel.to(device)\n\n# 📌 8. OPTIMIZER\nparams = [p for p in model.parameters() if p.requires_grad]\noptimizer = torch.optim.SGD(params, lr=LEARNING_RATE, momentum=0.9, weight_decay=0.0005)\nlr_scheduler = torch.optim.lr_scheduler.StepLR(optimizer, step_size=5, gamma=0.1)\n\n# 📌 9. TRAIN LOOP\nfor epoch in range(NUM_EPOCHS):\n    model.train()\n    running_loss = 0.0\n    \n    for images, targets in train_loader:\n        images = list(img.to(device) for img in images)\n        targets = [{k: v.to(device) for k, v in t.items()} for t in targets]\n        \n        loss_dict = model(images, targets)\n        losses = sum(loss for loss in loss_dict.values())\n        \n        optimizer.zero_grad()\n        losses.backward()\n        optimizer.step()\n        \n        running_loss += losses.item()\n    \n    lr_scheduler.step()\n    \n    print(f\"Epoch {epoch+1}, Loss: {running_loss/len(train_loader):.4f}\")\n\nprint(\"Training completed!\")\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-04-29T13:02:25.506019Z","iopub.status.idle":"2025-04-29T13:02:25.506412Z","shell.execute_reply":"2025-04-29T13:02:25.506223Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## Contrastive Model Training on Extracted Patches","metadata":{}},{"cell_type":"code","source":"# Paths to each condition's best weights\nspinal_stenosis_weights = (\n    \"results_spinal_canal_stenosis/Spinal_Canal_Stenosis/\"\n    \"epochs-1_batch-4_20250311_1303/weights/best.pt\"\n)\nneural_foraminal_weights = (\n    \"results_neural_foraminal_narrowing/Neural_Foraminal_Narrowing/\"\n    \"epochs-1_batch-4_20250311_1303/weights/best.pt\"\n)\nsubarticular_weights = (\n    \"results_subarticular_stenosis/Subarticular_Stenosis/\"\n    \"epochs-1_batch-4_20250311_1303/weights/best.pt\"\n)\n\n# Load each YOLO model\nmodel_spinal_stenosis = YOLO(spinal_stenosis_weights)\nmodel_neural_foraminal = YOLO(neural_foraminal_weights)\nmodel_subarticular = YOLO(subarticular_weights)\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-04-29T13:02:25.507515Z","iopub.status.idle":"2025-04-29T13:02:25.507786Z","shell.execute_reply":"2025-04-29T13:02:25.507678Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## Inference for Axial T2 Data\n\n\n","metadata":{}},{"cell_type":"code","source":"from ultralytics import YOLO","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-04-29T13:02:25.508732Z","iopub.status.idle":"2025-04-29T13:02:25.509039Z","shell.execute_reply":"2025-04-29T13:02:25.508931Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"RSNA_ROOT_DIR = \"/kaggle/input/rsna-2024-lumbar-spine-degenerative-classification\"\n\nLEVELS = ['l1_l2', 'l2_l3', 'l3_l4', 'l4_l5', 'l5_s1']\nCLASSES = ['Normal_Mild', 'Moderate', 'Severe']\n\nDATASET_TYPE = \"test\"\nTEST_SERIES_DESCRIPTIONS_CSV = f\"{DATASET_TYPE}_series_descriptions.csv\"\nTEST_IMAGES_ROOT_DIR = os.path.join(RSNA_ROOT_DIR, f\"{DATASET_TYPE}_images\")\nTEST_DF = pd.read_csv(os.path.join(RSNA_ROOT_DIR, TEST_SERIES_DESCRIPTIONS_CSV))\nDEBUG = len(TEST_DF.study_id.unique()) == 1\n\nYOLO_PT_AXIAL_T2_PATH = \"/kaggle/input/rsna24-pt-yolo-axial-t2/best.pt\"    \nSIAMESE_AXIAL_T2_REFIMG_ROOT_DIR = '/kaggle/input/rsna24-refimages-axial-t2-saimese/refimages'\nSIAMESE_AXIAL_T2_PT_LIST = sorted(glob.glob('/kaggle/input/rsna24-pt-axial-t2-siamese/to_upload/*.pth'))\n\nPATCH_IMAGES_DIR = \"/kaggle/working/patches\"\nos.makedirs(PATCH_IMAGES_DIR, exist_ok=True)\nAXIAL_T2_DIR = os.path.join(PATCH_IMAGES_DIR, \"Axial_T2\")\nos.makedirs(AXIAL_T2_DIR, exist_ok=True)\n\nDEVICE = torch.device(\"cuda\" if torch.cuda.is_available() else \"cpu\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-04-29T13:02:25.509652Z","iopub.status.idle":"2025-04-29T13:02:25.509932Z","shell.execute_reply":"2025-04-29T13:02:25.509808Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# Helper functions\n\ndef convert_to_dict(d: Any) -> Any:\n    \"\"\"\n    Recursively convert a defaultdict to a regular dict.\n\n    Args:\n        d: A dictionary or defaultdict.\n\n    Returns:\n        A regular dictionary with the same keys and values.\n    \"\"\"\n    if isinstance(d, defaultdict):\n        return {k: convert_to_dict(v) for k, v in d.items()}\n    return d\n\n\ndef atoi(text: str) -> Union[int, str]:\n    \"\"\"\n    Convert a string to an integer if it is numeric; otherwise, return the string.\n\n    Args:\n        text: The input string.\n\n    Returns:\n        The integer conversion or the original string.\n    \"\"\"\n    return int(text) if text.isdigit() else text\n\n\ndef natural_keys(text: str) -> List[Union[int, str]]:\n    \"\"\"\n    Generate keys to sort strings in human order.\n\n    Splits the text into integers and non-numeric parts.\n\n    Args:\n        text: The text to be split.\n\n    Returns:\n        A list of integers and strings for natural sorting.\n    \"\"\"\n    return [atoi(c) for c in re.split(r'(\\d+)', text)]\n\n\ndef convert_dicom_to_image(dcm_path: str) -> np.ndarray:\n    \"\"\"\n    Convert a DICOM file to a normalized 8-bit grayscale image.\n\n    The function applies modality and VOI LUTs, and adjusts for photometric interpretation.\n\n    Args:\n        dcm_path: Path to the DICOM file.\n\n    Returns:\n        The converted image as a NumPy array with dtype uint8.\n    \"\"\"\n    dicom = pydicom.dcmread(dcm_path)\n    arr = dicom.pixel_array\n\n    # Handle palette color images if necessary\n    if dicom.PhotometricInterpretation == \"PALETTE COLOR\":\n        arr = pydicom.pixel_data_handlers.apply_color_lut(arr, dicom)\n\n    arr = pydicom.pixel_data_handlers.apply_modality_lut(arr, dicom)\n    arr = pydicom.pixel_data_handlers.apply_voi_lut(arr, dicom, index=0)\n\n    if dicom.PhotometricInterpretation == \"MONOCHROME1\":\n        arr = np.amax(arr) - arr\n\n    lower, upper = np.percentile(arr, (1, 99))\n    arr = np.clip(arr, lower, upper)\n    arr = arr - np.min(arr)\n    arr = arr / np.max(arr)\n    arr = (arr * 255).astype(np.uint8)\n    return arr\n\n\ndef visualize_detections(image: np.ndarray, \n                         p0: Tuple[int, int], \n                         p1: Tuple[int, int], \n                         level: str, \n                         side: str) -> None:\n    \"\"\"\n    Visualize detection patches by drawing a rectangle and overlaying text on the image.\n\n    Args:\n        image: The input grayscale image.\n        p0: Top-left corner coordinates (x0, y0) of the rectangle.\n        p1: Bottom-right corner coordinates (x1, y1) of the rectangle.\n        level: The lumbar spine level (e.g. 'l1_l2').\n        side: Side of the detection (e.g. 'left' or 'right').\n    \"\"\"\n    image_rgb = cv2.cvtColor(image, cv2.COLOR_GRAY2BGR)\n    image_rgb = cv2.rectangle(image_rgb, p0, p1, color=(10, 10, 200), thickness=2)\n    plt.imshow(image_rgb)\n    text = f\"{level}_{side}\"\n    x_position, y_position = 50, 50  # Coordinates for text\n    plt.text(x_position, y_position, text, color='red', fontsize=15, \n             fontweight='bold', backgroundcolor='white')\n    plt.axis('off')\n    plt.show()\n\n\ndef save_patch(image: np.ndarray, \n               center: Tuple[int, int], \n               patch_size: int, \n               save_path: str) -> None:\n    \"\"\"\n    Extract a patch centered at the provided coordinates from the image and save it as a PNG.\n\n    Args:\n        image: The full image from which to extract the patch.\n        center: A tuple (xc, yc) indicating the center of the patch.\n        patch_size: The full width/height of the square patch.\n        save_path: The file path to save the extracted patch.\n    \"\"\"\n    xc, yc = center\n    half_patch = patch_size // 2\n    x0, y0 = int(xc) - half_patch, int(yc) - half_patch\n    x1, y1 = int(xc) + half_patch, int(yc) + half_patch\n    patch = image[y0:y1, x0:x1]\n    cv2.imwrite(save_path, patch)\n\n\ndef process_detection_results(dcm_conf_per_class: Dict[int, List[Tuple[float, float, float, float, float, np.ndarray, int]]]\n                             ) -> Dict[str, Dict[str, Tuple[float, float, float, float, float, np.ndarray, int]]]:\n    \"\"\"\n    Process raw detection outputs to select the best detections for each level and side.\n\n    Detections for each class are sorted by confidence and then partitioned by side (left/right).\n    Further logic compares left and right detections based on confidence scores for possible adjustments.\n\n    Args:\n        dcm_conf_per_class: A dictionary keyed by class_id that maps to a list of tuples containing\n            bounding box coordinates, confidence, image and DICOM number.\n\n    Returns:\n        A nested dictionary organized as best_dcm[level][side] containing the best detection tuple.\n    \"\"\"\n    sorted_dcm: Dict[int, Any] = defaultdict(dict)\n    for class_id, detections in dcm_conf_per_class.items():\n        if detections:\n            detections = sorted(detections, key=lambda x: x[4])\n            side: str = \"left\" if class_id < 5 else \"right\"\n            level: str = LEVELS[class_id % 5]\n            sorted_dcm[level][side] = detections\n\n    best_dcm: Dict[str, Dict[str, Tuple[float, float, float, float, float, np.ndarray, int]]] = defaultdict(dict)\n    for level in sorted_dcm:\n        if 'left' in sorted_dcm[level]:\n            best_dcm[level]['left'] = sorted_dcm[level]['left'][-1]\n        if 'right' in sorted_dcm[level]:\n            best_dcm[level]['right'] = sorted_dcm[level]['right'][-1]\n\n        # Adjust detection if there is high confidence on one side and low on the other\n        if 'left' in sorted_dcm[level] and 'right' in sorted_dcm[level]:\n            left_best = sorted_dcm[level]['left'][-1]\n            right_best = sorted_dcm[level]['right'][-1]\n            if left_best[4] > 0.5 and right_best[4] < 0.5:\n                best_dcm_number = left_best[-1]\n                for det in sorted_dcm[level]['right']:\n                    if det[-1] == best_dcm_number:\n                        best_dcm[level]['right'] = det\n                        break\n                else:\n                    # Use left image and dcm number if matching detection not found\n                    best_dcm[level]['right'] = (right_best[0], right_best[1], \n                                                right_best[2], right_best[3], \n                                                0, left_best[-2], left_best[-1])\n            elif left_best[4] < 0.5 and right_best[4] > 0.5:\n                best_dcm_number = right_best[-1]\n                for det in sorted_dcm[level]['left']:\n                    if det[-1] == best_dcm_number:\n                        best_dcm[level]['left'] = det\n                        break\n                else:\n                    best_dcm[level]['left'] = (left_best[0], left_best[1], \n                                               left_best[2], left_best[3], \n                                               0, right_best[-2], right_best[-1])\n    return convert_to_dict(best_dcm)\n\n\ndef save_patch_with_metadata(image: np.ndarray, \n                             center: Tuple[int, int], \n                             patch_size: int, \n                             save_path: str) -> None:\n    \"\"\"\n    Extract a patch from the full image, save it, and save its bounding box as metadata.\n    \"\"\"\n    xc, yc = center\n    half_patch = patch_size // 2\n    x0, y0 = int(xc) - half_patch, int(yc) - half_patch\n    x1, y1 = int(xc) + half_patch, int(yc) + half_patch\n    patch = image[y0:y1, x0:x1]\n    cv2.imwrite(save_path, patch)\n    \n    # Save metadata.\n    metadata = {\n        \"center\": [xc, yc],\n        \"patch_size\": patch_size,\n        \"bbox\": [x0, y0, x1, y1]\n    }\n    metadata_path = Path(save_path).with_suffix(\".json\")\n    with open(metadata_path, \"w\") as f:\n        json.dump(metadata, f)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-04-29T13:02:25.510979Z","iopub.status.idle":"2025-04-29T13:02:25.511285Z","shell.execute_reply":"2025-04-29T13:02:25.511154Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"def extract_axial_t2_patches() -> None:\n    \"\"\"\n    Extract and save axial T2 patches from DICOM images.\n\n    For each study and series having the \"Axial T2\" description, the function:\n      - Reads DICOM files.\n      - Converts DICOM images to an 8-bit format.\n      - Uses a YOLO detector (loaded externally) to detect regions of interest.\n      - Selects the best detections for each spine level and side.\n      - Extracts and saves a patch around the detected region.\n      - Optionally visualizes detections if DEBUG is True.\n    \"\"\"\n    # Load the YOLO detector (assumes YOLO class is available in the environment)\n    detector = YOLO(YOLO_PT_AXIAL_T2_PATH)\n\n    study_ids = TEST_DF.study_id.unique()\n    for study_id in tqdm(study_ids, desc=\"Processing studies\"):\n        df_study = TEST_DF[(TEST_DF.study_id == study_id) & (TEST_DF.series_description == \"Axial T2\")]\n        if not df_study.empty:\n            axial_t2_series_ids = df_study['series_id'].unique()\n            for series_id in axial_t2_series_ids:\n                read_dir = os.path.join(TEST_IMAGES_ROOT_DIR, str(study_id), str(series_id))\n                dcm_paths = sorted(glob.glob(os.path.join(read_dir, \"*.dcm\")), key=natural_keys)\n                \n                # Dictionary to store detections per class\n                dcm_conf_per_class: Dict[int, List[Tuple[float, float, float, float, float, np.ndarray, int]]] = defaultdict(list)\n                \n                # Process each DICOM file in the series\n                for dcm_path in dcm_paths:\n                    image = convert_dicom_to_image(dcm_path)\n                    # Convert grayscale image to RGB for detector\n                    image_rgb = cv2.cvtColor(image, cv2.COLOR_GRAY2RGB)\n                    detection = detector.predict(source=image_rgb, verbose=False)\n                    \n                    # Process detections\n                    for row in detection[0].boxes.data:\n                        row_np = row.cpu().numpy()\n                        class_id: int = int(row_np[-1])\n                        # Append tuple: (x0, y0, x1, y1, confidence, image, dcm_number)\n                        dcm_conf_per_class[class_id].append((\n                            row_np[0], row_np[1], row_np[2], row_np[3], row_np[4], image, int(Path(dcm_path).stem)\n                        ))\n                \n                best_detections = process_detection_results(dcm_conf_per_class)\n                \n                # Save patches for each level and side\n                for level, sides in best_detections.items():\n                    for side, data in sides.items():\n                        if DEBUG:\n                            print(level, side, data[0], data[1], data[2], data[3], data[4], data[-1])\n                        x0, y0, x1, y1, _, image_ref, dcm_number = data\n                        xc = int(np.round((x0 + x1) / 2.0))\n                        yc = int(np.round((y0 + y1) / 2.0))\n                        width = image_ref.shape[1]\n                        # Define patch size as 10% of image width (patch half size computed accordingly)\n                        patch_size_half = int(np.round(0.1 * width / 2))\n                        patch_size = patch_size_half * 2\n                        \n                        target_dir = os.path.join(AXIAL_T2_DIR, str(study_id), level)\n                        os.makedirs(target_dir, exist_ok=True)\n                        png_path = os.path.join(target_dir, f\"{study_id}_{series_id}_{side}_{dcm_number:04}.png\")\n                        save_patch_with_metadata(image_ref, (xc, yc), patch_size, png_path)\n                        # save_patch(image_ref, (xc, yc), patch_size, png_path)\n                        \n                        if DEBUG:\n                            visualize_detections(image_ref, (xc - patch_size_half, yc - patch_size_half),\n                                                 (xc + patch_size_half, yc + patch_size_half), level, side)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-04-29T13:02:25.511893Z","iopub.status.idle":"2025-04-29T13:02:25.512224Z","shell.execute_reply":"2025-04-29T13:02:25.512049Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"extract_axial_t2_patches()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-04-29T13:02:25.513099Z","iopub.status.idle":"2025-04-29T13:02:25.513513Z","shell.execute_reply":"2025-04-29T13:02:25.513321Z"}},"outputs":[],"execution_count":null}]}