{"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":"gpu","dataSources":[{"sourceId":52254,"databundleVersionId":9674523,"sourceType":"competition"},{"sourceId":6211844,"sourceType":"datasetVersion","datasetId":3567114},{"sourceId":6091,"sourceType":"modelInstanceVersion","modelInstanceId":4623,"modelId":2800}],"isInternetEnabled":false,"language":"python","sourceType":"notebook","isGpuEnabled":true}},"nbformat_minor":4,"nbformat":4,"cells":[{"cell_type":"markdown","source":"# Training Notebook\n\n# RSNA 2023 Abdominal Trauma Detection\n\nThis notebook walks you through how to train a **Convolutional Neural Network (CNN)** model using Keras (Core and CV) on the RSNA 2023 Abdominal Trauma Detection dataset made available for this competition.\n","metadata":{}},{"cell_type":"markdown","source":"# Setup and Imports\n","metadata":{}},{"cell_type":"code","source":"# ! pip install -q git+https://github.com/keras-team/keras-cv","metadata":{"_kg_hide-output":true,"_kg_hide-input":false,"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import os\nos.environ[\"KERAS_BACKEND\"] = \"tensorflow\"\n\nimport keras\nimport keras_cv\nfrom keras.models import Model\nfrom keras.layers import Input, Dense, GlobalAveragePooling2D\nfrom keras.models import Model\nfrom keras.optimizers.schedules import CosineDecay\nimport gc\nimport cv2\nfrom pydicom.pixel_data_handlers.util import apply_modality_lut\nfrom joblib import Parallel, delayed\n\nimport pydicom\nimport numpy as np\nimport pandas as pd\nimport tensorflow as tf\nfrom matplotlib import pyplot as plt\nfrom sklearn.model_selection import train_test_split\nfrom tqdm.notebook import tqdm\nfrom glob import glob","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-03-24T17:19:13.26917Z","iopub.execute_input":"2025-03-24T17:19:13.269535Z","iopub.status.idle":"2025-03-24T17:19:13.274875Z","shell.execute_reply.started":"2025-03-24T17:19:13.269507Z","shell.execute_reply":"2025-03-24T17:19:13.273991Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"# **EDA**\n\nReference Notebook: https://www.kaggle.com/code/aritrag/eda-train-csv\n\n**Key observations related to the data**:\n* **Healthy columns corelation**: The correlations between different health columns are generally small, indicating that the healthy state of one organ might not be strongly related to the healthy state of other organs.\n* **Injury corelation:** There are some specific correlations between individual injury types, such as the correlation between extravasation_injury and spleen_high. However, the correlation between different organs' injuries is generally low, which might suggest that injuries to different organs occur independently of each other.\n \n* **Relation between class**: bowel_injury and bowel_healthy are complementary. Their probabilities add up to 1.0.\nSimilarly, extravasation_injury and extravasation_healthy are complementary.\n\n* **Simplification**: For the model, only {bowel/extravasation}_injury will be included, and the corresponding healthy status can be calculated using a sigmoid function.\n\n* **Softmax**: {kidney/liver/spleen}_{healthy/low/high} classifications are softmaxed, ensuring their combined probabilities sum up to 1.0 for each organ, simplifying the model while preserving essential information.","metadata":{}},{"cell_type":"markdown","source":"# Configuration Class","metadata":{}},{"cell_type":"code","source":"class Config:\n    SEED = 42\n    IMAGE_SIZE = [256, 256]\n    BATCH_SIZE = 64\n    EPOCHS = 10\n    RESIZE_DIM = 256\n    TARGET_COLS  = [\n        \"bowel_injury\", \"extravasation_injury\",\n        \"kidney_healthy\", \"kidney_low\", \"kidney_high\",\n        \"liver_healthy\", \"liver_low\", \"liver_high\",\n        \"spleen_healthy\", \"spleen_low\", \"spleen_high\",\n    ]\n    AUTOTUNE = tf.data.AUTOTUNE\n\nconfig = Config()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-03-24T16:59:56.041151Z","iopub.execute_input":"2025-03-24T16:59:56.041466Z","iopub.status.idle":"2025-03-24T16:59:56.046556Z","shell.execute_reply.started":"2025-03-24T16:59:56.041443Z","shell.execute_reply":"2025-03-24T16:59:56.0456Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"# Reproductibility ","metadata":{}},{"cell_type":"code","source":"keras.utils.set_random_seed(seed=config.SEED)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-03-24T16:59:58.915383Z","iopub.execute_input":"2025-03-24T16:59:58.915709Z","iopub.status.idle":"2025-03-24T16:59:58.919634Z","shell.execute_reply.started":"2025-03-24T16:59:58.915684Z","shell.execute_reply":"2025-03-24T16:59:58.918819Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"# Data set\n\nThis train.csv file contains details about the dataset, stored in a table-like format. Here's what each column means:abs\n\n**patient_id:** A unique identifier for each patient. This ensures that each patient’s data can be easily tracked.\n\n**series_id**: A unique identifier for each scan (series of images) for a patient. A patient can have multiple scans.\n\n**instance_number**: The specific image number in a scan. Medical scans consist of multiple \"slices,\" and this number helps identify them.\n\n**[bowel/extravasation]_[healthy/injury]:** These columns indicate whether the patient has bowel or extravasation injuries (binary targets: 0 for healthy, 1 for injury).\n\n**[kidney/liver/spleen]_[healthy/low/high]:** These columns specify the injury severity for organs:\n* healthy: No injury\n* low: Mild injury\n* high: Severe injury\n* any_injury: A binary column indicating if the patient has any injury at all (1 for injured, 0 for healthy).\n\n\n### **NOTE FOR ME:** For Series and instances we use image_level_labels\n","metadata":{}},{"cell_type":"code","source":"BASE_PATH = f\"/kaggle/input/rsna-atd-512x512-png-v2-dataset\"","metadata":{"_kg_hide-input":true,"trusted":true,"execution":{"iopub.status.busy":"2025-03-24T17:00:18.897005Z","iopub.execute_input":"2025-03-24T17:00:18.897341Z","iopub.status.idle":"2025-03-24T17:00:18.901094Z","shell.execute_reply.started":"2025-03-24T17:00:18.897321Z","shell.execute_reply":"2025-03-24T17:00:18.900139Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# train\ndataframe = pd.read_csv(f\"{BASE_PATH}/train.csv\")\ndataframe[\"image_path\"] = f\"{BASE_PATH}/train_images\"\\\n                    + \"/\" + dataframe.patient_id.astype(str)\\\n                    + \"/\" + dataframe.series_id.astype(str)\\\n                    + \"/\" + dataframe.instance_number.astype(str) +\".png\"\ndataframe = dataframe.drop_duplicates()\n\ndataframe.head(2)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-03-24T17:00:20.995665Z","iopub.execute_input":"2025-03-24T17:00:20.996015Z","iopub.status.idle":"2025-03-24T17:00:21.145131Z","shell.execute_reply.started":"2025-03-24T17:00:20.995989Z","shell.execute_reply":"2025-03-24T17:00:21.144202Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"# Dataset¶\nThe dataset provided in the competition consists of DICOM images. We will not be training on the DICOM images, rather would work on PNG image which are extracted from the DICOM format.\n\n[A helpful resource on the conversion of DICOM to PNG](http://https://www.kaggle.com/code/radek1/how-to-process-dicom-images-to-pngs)","metadata":{}},{"cell_type":"markdown","source":"We split the training dataset into train and validation. This is a common practise in the Machine Learning pipelines. We not only want to train our model, but also want to validate it's training.\n\nA small catch here is that the training and validation data should have an aligned data distribution. Here we handle that by grouping the lables and then splitting the dataset. This ensures an aligned data distribution between the training and the validation splits","metadata":{}},{"cell_type":"code","source":"# Function to handle the split for each group\ndef split_group(group, test_size=0.2):\n    if len(group) == 1:\n        return (group, pd.DataFrame()) if np.random.rand() < test_size else (pd.DataFrame(), group)\n    else:\n        return train_test_split(group, test_size=test_size, random_state=42)\n\n# Initialize the train and validation datasets\ntrain_data = pd.DataFrame()\nval_data = pd.DataFrame()\n\n# Iterate through the groups and split them, handling single-sample groups\nfor _, group in dataframe.groupby(config.TARGET_COLS):\n    train_group, val_group = split_group(group)\n    train_data = pd.concat([train_data, train_group], ignore_index=True)\n    val_data = pd.concat([val_data, val_group], ignore_index=True)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-03-24T17:00:24.550909Z","iopub.execute_input":"2025-03-24T17:00:24.551224Z","iopub.status.idle":"2025-03-24T17:00:24.618379Z","shell.execute_reply.started":"2025-03-24T17:00:24.551203Z","shell.execute_reply":"2025-03-24T17:00:24.617646Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"train_data.shape, val_data.shape","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-03-24T17:00:26.384639Z","iopub.execute_input":"2025-03-24T17:00:26.385002Z","iopub.status.idle":"2025-03-24T17:00:26.390386Z","shell.execute_reply.started":"2025-03-24T17:00:26.384976Z","shell.execute_reply":"2025-03-24T17:00:26.389536Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"# Data Pipeline\n\nTensorFlow tf.data pipeline is built to handle large datasets efficiently by batching, shuffling, and prefetching data for model training\n\nUsing tf.data we can map out data to an augmentation pipeline simple by using the map API.\n\n**Preprocessing ensures** that the input images are resized, normalized, and correctly paired with labels.\n\n**Augmentation** improves the robustness of the model by artificially increasing the diversity of the dataset.\n\n**Pipeline Optimization with tf.data** ensures that data is processed efficiently, especially for large datasets, by leveraging parallelism and prefetching.","metadata":{}},{"cell_type":"code","source":"def decode_image_and_label(image_path, label):\n    file_bytes = tf.io.read_file(image_path)\n    image = tf.io.decode_png(file_bytes, channels=3, dtype=tf.uint8)\n    image = tf.image.resize(image, config.IMAGE_SIZE, method=\"bilinear\")\n    image = tf.cast(image, tf.float32) / 255.0\n    \n    label = tf.cast(label, tf.float32)\n    #         bowel       fluid       kidney      liver       spleen\n    labels = (label[0:1], label[1:2], label[2:5], label[5:8], label[8:11])\n    \n    return (image, labels)\n\n\ndef apply_augmentation(images, labels):\n    augmenter = keras_cv.layers.Augmenter(\n        [\n            keras_cv.layers.RandomFlip(mode=\"horizontal_and_vertical\"),\n            keras_cv.layers.RandomCutout(height_factor=0.2, width_factor=0.2),\n            \n        ]\n    )\n    return (augmenter(images), labels)\n\n\ndef build_dataset(image_paths, labels):\n    ds = (\n        tf.data.Dataset.from_tensor_slices((image_paths, labels))\n        .map(decode_image_and_label, num_parallel_calls=config.AUTOTUNE)\n        .shuffle(config.BATCH_SIZE * 10)\n        .batch(config.BATCH_SIZE)\n        .map(apply_augmentation, num_parallel_calls=config.AUTOTUNE)\n        .prefetch(config.AUTOTUNE)\n    )\n    return ds","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-03-24T17:00:28.903029Z","iopub.execute_input":"2025-03-24T17:00:28.903436Z","iopub.status.idle":"2025-03-24T17:00:28.912562Z","shell.execute_reply.started":"2025-03-24T17:00:28.903405Z","shell.execute_reply":"2025-03-24T17:00:28.911657Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"paths  = train_data.image_path.tolist()\nlabels = train_data[config.TARGET_COLS].values\n\nds = build_dataset(image_paths=paths, labels=labels)\nimages, labels = next(iter(ds))\nimages.shape, [label.shape for label in labels]","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-03-24T17:00:33.623169Z","iopub.execute_input":"2025-03-24T17:00:33.623461Z","iopub.status.idle":"2025-03-24T17:00:40.127763Z","shell.execute_reply.started":"2025-03-24T17:00:33.62344Z","shell.execute_reply":"2025-03-24T17:00:40.126799Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# No more customizing your plots by hand, KerasCV has your back ;)\nkeras_cv.visualization.plot_image_gallery(\n    images=images,\n    value_range=(0, 1),\n    rows=2,\n    cols=2,\n)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-03-24T17:00:40.129094Z","iopub.execute_input":"2025-03-24T17:00:40.12947Z","iopub.status.idle":"2025-03-24T17:00:40.613889Z","shell.execute_reply.started":"2025-03-24T17:00:40.129438Z","shell.execute_reply":"2025-03-24T17:00:40.61286Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"# Build Model\n\nWe are going to load a pretrained model from the [list of avaiable backbones in KerasCV](https://keras.io/api/keras_cv/models/backbones/). We are using the `ResNetBackbone` as our backbone. The practise of using a pretrained model and finetuning it to a specific dataset is prevalent in the DL community.\n\nWe use the [Functional API](https://keras.io/guides/functional_api/) of Keras to build the model. The design of the model would be such that we input a single image and we get different heads for the various predictions we need (kidney, spleen...).\n\nWe have also added a Learning Rate scheduler for you to work with. When an athlete trains, the first step is always to warm up. We take a similar approach to training our models. We warm up with model where the learning rate increses from the initial LR to a higher LR. After the warmup stage we provide a decay algorithm (cosine here). A list of all the learning rate scheduler can be found [here](https://keras.io/api/optimizers/learning_rate_schedules/).","metadata":{}},{"cell_type":"code","source":"import keras\nimport keras_cv\nfrom keras.layers import Input, Dense, GlobalAveragePooling2D\nfrom keras.models import Model\nfrom keras.optimizers.schedules import CosineDecay\n\ndef build_model(warmup_steps, decay_steps):\n    # Define Input Tensor\n    inputs = Input(shape=(config.IMAGE_SIZE[0], config.IMAGE_SIZE[1], 3))  # Removed batch_size\n\n    # Load ResNet Backbone and Wrap in a Keras Layer\n    backbone = keras_cv.models.ResNetBackbone.from_preset(\"resnet50_imagenet\")  \n    backbone_output = backbone(inputs)  # Ensures the backbone processes the correct input\n\n    # Global Average Pooling to Reduce Dimensions\n    gap = GlobalAveragePooling2D()(backbone_output)\n\n    # Define 'necks' for each injury type\n    x_bowel = Dense(32, activation='silu')(gap)\n    x_extra = Dense(32, activation='silu')(gap)\n    x_liver = Dense(32, activation='silu')(gap)\n    x_kidney = Dense(32, activation='silu')(gap)\n    x_spleen = Dense(32, activation='silu')(gap)\n\n    # Define Output Heads for Multi-Label Classification\n    out_bowel = Dense(1, name='bowel', activation='sigmoid')(x_bowel)\n    out_extra = Dense(1, name='extra', activation='sigmoid')(x_extra)\n    out_liver = Dense(3, name='liver', activation='softmax')(x_liver)\n    out_kidney = Dense(3, name='kidney', activation='softmax')(x_kidney)\n    out_spleen = Dense(3, name='spleen', activation='softmax')(x_spleen)\n\n    # Combine all outputs\n    outputs = [out_bowel, out_extra, out_liver, out_kidney, out_spleen]\n\n    # Create Model\n    print(\"[INFO] Building the model...\")\n    model = Model(inputs=inputs, outputs=outputs)\n\n    # Define Cosine Decay Learning Rate Schedule\n    cosine_decay = CosineDecay(\n        initial_learning_rate=1e-4,\n        decay_steps=decay_steps,\n        alpha=0.0\n    )\n\n    \n    # Compile Model with Optimizer, Loss, and Metrics\n    optimizer = keras.optimizers.Adam(learning_rate=cosine_decay)\n    loss = {\n        \"bowel\": keras.losses.BinaryCrossentropy(),\n        \"extra\": keras.losses.BinaryCrossentropy(),\n        \"liver\": keras.losses.CategoricalCrossentropy(),\n        \"kidney\": keras.losses.CategoricalCrossentropy(),\n        \"spleen\": keras.losses.CategoricalCrossentropy(),\n    }\n    metrics = {\n        \"bowel\": [\"accuracy\"],\n        \"extra\": [\"accuracy\"],\n        \"liver\": [\"accuracy\"],\n        \"kidney\": [\"accuracy\"],\n        \"spleen\": [\"accuracy\"],\n    }\n    \n    print(\"[INFO] Compiling the model...\")\n    model.compile(optimizer=optimizer, loss=loss, metrics=metrics)\n\n    return model\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-03-24T17:00:54.937992Z","iopub.execute_input":"2025-03-24T17:00:54.938287Z","iopub.status.idle":"2025-03-24T17:00:54.947254Z","shell.execute_reply.started":"2025-03-24T17:00:54.938267Z","shell.execute_reply":"2025-03-24T17:00:54.946329Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"# Train the model with \"model.fit\"","metadata":{}},{"cell_type":"code","source":"# get image_paths and labels\nprint(\"[INFO] Building the dataset...\")\ntrain_paths = train_data.image_path.values; train_labels = train_data[config.TARGET_COLS].values.astype(np.float32)\nvalid_paths = val_data.image_path.values; valid_labels = val_data[config.TARGET_COLS].values.astype(np.float32)\n\n# train and valid dataset\ntrain_ds = build_dataset(image_paths=train_paths, labels=train_labels)\nval_ds = build_dataset(image_paths=valid_paths, labels=valid_labels)\n\ntotal_train_steps = train_ds.cardinality().numpy() * config.BATCH_SIZE * config.EPOCHS\nwarmup_steps = int(total_train_steps * 0.10)\ndecay_steps = total_train_steps - warmup_steps\n\nprint(f\"{total_train_steps=}\")\nprint(f\"{warmup_steps=}\")\nprint(f\"{decay_steps=}\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-03-24T17:01:01.739646Z","iopub.execute_input":"2025-03-24T17:01:01.740103Z","iopub.status.idle":"2025-03-24T17:01:02.645148Z","shell.execute_reply.started":"2025-03-24T17:01:01.740065Z","shell.execute_reply":"2025-03-24T17:01:02.644427Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# build the model\nprint(\"[INFO] Building the model...\")\nmodel = build_model(warmup_steps, decay_steps)\n\n# train\nprint(\"[INFO] Training...\")\nhistory = model.fit(\n    train_ds,\n    epochs=config.EPOCHS,\n    validation_data=val_ds,\n)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-03-24T17:01:04.11603Z","iopub.execute_input":"2025-03-24T17:01:04.116309Z","iopub.status.idle":"2025-03-24T17:15:26.882193Z","shell.execute_reply.started":"2025-03-24T17:01:04.116289Z","shell.execute_reply":"2025-03-24T17:15:26.881371Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## Visualize the training plots","metadata":{}},{"cell_type":"code","source":"# Create a 3x2 grid for the subplots\nfig, axes = plt.subplots(5, 1, figsize=(5, 15))\n\n# Flatten axes to iterate through them\naxes = axes.flatten()\n\n# Iterate through the metrics and plot them\nfor i, name in enumerate([\"bowel\", \"extra\", \"kidney\", \"liver\", \"spleen\"]):\n    # Plot training accuracy\n    axes[i].plot(history.history[name + '_accuracy'], label='Training ' + name)\n    # Plot validation accuracy\n    axes[i].plot(history.history['val_' + name + '_accuracy'], label='Validation ' + name)\n    axes[i].set_title(name)\n    axes[i].set_xlabel('Epoch')\n    axes[i].set_ylabel('Accuracy')\n    axes[i].legend()\n\nplt.tight_layout()\nplt.show()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-03-24T17:16:45.083517Z","iopub.execute_input":"2025-03-24T17:16:45.083833Z","iopub.status.idle":"2025-03-24T17:16:45.967299Z","shell.execute_reply.started":"2025-03-24T17:16:45.08381Z","shell.execute_reply":"2025-03-24T17:16:45.96646Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"plt.plot(history.history[\"loss\"], label=\"loss\")\nplt.plot(history.history[\"val_loss\"], label=\"val loss\")\nplt.legend()\nplt.show()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-03-24T17:16:53.212342Z","iopub.execute_input":"2025-03-24T17:16:53.212635Z","iopub.status.idle":"2025-03-24T17:16:53.355716Z","shell.execute_reply.started":"2025-03-24T17:16:53.212613Z","shell.execute_reply":"2025-03-24T17:16:53.35492Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# store best results\nbest_epoch = np.argmin(history.history['val_loss'])\nbest_loss = history.history['val_loss'][best_epoch]\nbest_acc_bowel = history.history['val_bowel_accuracy'][best_epoch]\nbest_acc_extra = history.history['val_extra_accuracy'][best_epoch]\nbest_acc_liver = history.history['val_liver_accuracy'][best_epoch]\nbest_acc_kidney = history.history['val_kidney_accuracy'][best_epoch]\nbest_acc_spleen = history.history['val_spleen_accuracy'][best_epoch]\n\n# Find mean accuracy\nbest_acc = np.mean(\n    [best_acc_bowel,\n     best_acc_extra,\n     best_acc_liver,\n     best_acc_kidney,\n     best_acc_spleen\n])\n\n\nprint(f'>>>> BEST Loss  : {best_loss:.3f}\\n>>>> BEST Acc   : {best_acc:.3f}\\n>>>> BEST Epoch : {best_epoch}\\n')\nprint('ORGAN Acc:')\nprint(f'  >>>> {\"Bowel\".ljust(15)} : {best_acc_bowel:.3f}')\nprint(f'  >>>> {\"Extravasation\".ljust(15)} : {best_acc_extra:.3f}')\nprint(f'  >>>> {\"Liver\".ljust(15)} : {best_acc_liver:.3f}')\nprint(f'  >>>> {\"Kidney\".ljust(15)} : {best_acc_kidney:.3f}')\nprint(f'  >>>> {\"Spleen\".ljust(15)} : {best_acc_spleen:.3f}')","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-03-24T17:17:00.664301Z","iopub.execute_input":"2025-03-24T17:17:00.664587Z","iopub.status.idle":"2025-03-24T17:17:00.672475Z","shell.execute_reply.started":"2025-03-24T17:17:00.664566Z","shell.execute_reply":"2025-03-24T17:17:00.671631Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"# Store the model for inference","metadata":{}},{"cell_type":"code","source":"# Save the model\nmodel.save(\"rsna-atd.keras\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-03-24T17:17:04.989999Z","iopub.execute_input":"2025-03-24T17:17:04.990291Z","iopub.status.idle":"2025-03-24T17:17:06.724036Z","shell.execute_reply.started":"2025-03-24T17:17:04.990271Z","shell.execute_reply":"2025-03-24T17:17:06.723021Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"# Data Pipeline\n\nWe have a model that accepts image in the PNG format. We will now convert the DICOM images to PNG for the model to infer.","metadata":{}},{"cell_type":"code","source":"# Constants\nSTRIDE = 10\nBASEPATH = \"/kaggle/input/rsna-2023-abdominal-trauma-detection\"\nIMAGE_DIR = \"/tmp/dataset/rsna-atd\"\nMODEL_PATH = \"/kaggle/working/rsna-atd.keras\"\n\n# INPUT_MODEL_PATH = \"/kaggle/input/kerascv-starter-notebook-train/rsna-atd.keras\"\n\n# Load metadata\nmeta_df = pd.read_csv(f\"{BASEPATH}/test_series_meta.csv\")\n\n# Sanity check on unique patients\nprint(f\"num_rows={meta_df.shape[0]}\")\nprint(f\"unique_patients={meta_df['patient_id'].nunique()}\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-03-24T17:17:09.743097Z","iopub.execute_input":"2025-03-24T17:17:09.743389Z","iopub.status.idle":"2025-03-24T17:17:09.752258Z","shell.execute_reply.started":"2025-03-24T17:17:09.743367Z","shell.execute_reply":"2025-03-24T17:17:09.751465Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# Create DICOM folder paths\nmeta_df[\"dicom_folder\"] = (\n    BASEPATH + \"/test_images/\" +\n    meta_df.patient_id.astype(str) + \"/\" +\n    meta_df.series_id.astype(str)\n)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-03-24T17:17:15.126281Z","iopub.execute_input":"2025-03-24T17:17:15.126604Z","iopub.status.idle":"2025-03-24T17:17:15.131946Z","shell.execute_reply.started":"2025-03-24T17:17:15.126577Z","shell.execute_reply":"2025-03-24T17:17:15.13104Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# Get list of .dcm image paths (strided)\ntest_folders = meta_df.dicom_folder.tolist()\ntest_paths = []\nfor folder in tqdm(test_folders):\n    test_paths += sorted(glob(os.path.join(folder, \"*.dcm\")))[::STRIDE]\n\n# Build a DataFrame of test slices\ntest_df = pd.DataFrame(test_paths, columns=[\"dicom_path\"])\ntest_df[\"patient_id\"] = test_df.dicom_path.map(lambda x: x.split(\"/\")[-3]).astype(int)\ntest_df[\"series_id\"] = test_df.dicom_path.map(lambda x: x.split(\"/\")[-2]).astype(int)\ntest_df[\"instance_number\"] = test_df.dicom_path.map(lambda x: x.split(\"/\")[-1].replace(\".dcm\", \"\")).astype(int)\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-03-24T17:17:17.676818Z","iopub.execute_input":"2025-03-24T17:17:17.677182Z","iopub.status.idle":"2025-03-24T17:17:17.720564Z","shell.execute_reply.started":"2025-03-24T17:17:17.677155Z","shell.execute_reply":"2025-03-24T17:17:17.719681Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# Path where PNGs will be saved (for inference if needed)\ntest_df[\"image_path\"] = (\n    IMAGE_DIR + \"/test_images/\" +\n    test_df.patient_id.astype(str) + \"/\" +\n    test_df.series_id.astype(str) + \"/\" +\n    test_df.instance_number.astype(str) + \".png\"\n)\n\n# Preview\ntest_df.head(2)\n\n# Check dataset structure\nprint(f\"num_rows={test_df.shape[0]}\")\nprint(f\"unique_patients={test_df['patient_id'].nunique()}\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-03-24T17:17:21.245646Z","iopub.execute_input":"2025-03-24T17:17:21.246011Z","iopub.status.idle":"2025-03-24T17:17:21.253456Z","shell.execute_reply.started":"2025-03-24T17:17:21.245981Z","shell.execute_reply":"2025-03-24T17:17:21.252611Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"test_df.head(2)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-03-24T17:17:24.054541Z","iopub.execute_input":"2025-03-24T17:17:24.054882Z","iopub.status.idle":"2025-03-24T17:17:24.063145Z","shell.execute_reply.started":"2025-03-24T17:17:24.054837Z","shell.execute_reply":"2025-03-24T17:17:24.06232Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## DICOM to PNG pipeline","metadata":{}},{"cell_type":"code","source":"# Clear and recreate image directories\n!rm -r {IMAGE_DIR}\nos.makedirs(f\"{IMAGE_DIR}/train_images\", exist_ok=True)\nos.makedirs(f\"{IMAGE_DIR}/test_images\", exist_ok=True)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-03-24T17:17:27.755078Z","iopub.execute_input":"2025-03-24T17:17:27.755356Z","iopub.status.idle":"2025-03-24T17:17:28.109266Z","shell.execute_reply.started":"2025-03-24T17:17:27.755336Z","shell.execute_reply":"2025-03-24T17:17:28.108244Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# Standardize pixel values from DICOM\ndef standardize_pixel_array(dcm):\n    pixel_array = dcm.pixel_array\n    if dcm.PixelRepresentation == 1:\n        bit_shift = dcm.BitsAllocated - dcm.BitsStored\n        dtype = pixel_array.dtype \n        new_array = (pixel_array << bit_shift).astype(dtype) >> bit_shift\n        pixel_array = apply_modality_lut(new_array, dcm)\n    return pixel_array\n\n# Read and normalize DICOM image\ndef read_xray(path, fix_monochrome=True):\n    dicom = pydicom.dcmread(path)\n    data = standardize_pixel_array(dicom)\n    data = data - np.min(data)\n    data = data / (np.max(data) + 1e-5)\n    if fix_monochrome and dicom.PhotometricInterpretation == \"MONOCHROME1\":\n        data = 1.0 - data\n    return data\n\n# Resize and save PNG from DICOM path\ndef resize_and_save(file_path):\n    img = read_xray(file_path)\n    img = cv2.resize(img, (config.RESIZE_DIM, config.RESIZE_DIM), cv2.INTER_LINEAR)\n    img = (img * 255).astype(np.uint8)\n\n    sub_path = file_path.split(\"/\", 4)[-1].replace(\".dcm\", \".png\")\n    new_path = os.path.join(IMAGE_DIR, sub_path)\n    os.makedirs(os.path.dirname(new_path), exist_ok=True)\n    cv2.imwrite(new_path, img)\n    return\n\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-03-24T17:17:31.084392Z","iopub.execute_input":"2025-03-24T17:17:31.084724Z","iopub.status.idle":"2025-03-24T17:17:31.093349Z","shell.execute_reply.started":"2025-03-24T17:17:31.0847Z","shell.execute_reply":"2025-03-24T17:17:31.092376Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# Convert all test DICOMs to PNGs\nfile_paths = test_df.dicom_path.tolist()\n_ = Parallel(n_jobs=2, backend=\"threading\")(\n    delayed(resize_and_save)(file_path) for file_path in tqdm(file_paths, leave=True, position=0)\n)\n\ndel _; gc.collect()\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-03-24T17:19:25.079041Z","iopub.execute_input":"2025-03-24T17:19:25.079325Z","iopub.status.idle":"2025-03-24T17:19:25.809463Z","shell.execute_reply.started":"2025-03-24T17:19:25.079305Z","shell.execute_reply":"2025-03-24T17:19:25.808687Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## Building the tf.data pipeline","metadata":{}},{"cell_type":"code","source":"# Decode PNG image to Tensor\ndef decode_image(image_path):\n    file_bytes = tf.io.read_file(image_path)\n    image = tf.io.decode_png(file_bytes, channels=3, dtype=tf.uint8)\n    image = tf.image.resize(image, config.IMAGE_SIZE, method=\"bilinear\")\n    image = tf.cast(image, tf.float32) / 255.0\n    return image\n\n# Build batched tf.data.Dataset\ndef build_dataset(image_paths):\n    ds = (\n        tf.data.Dataset.from_tensor_slices(image_paths)\n        .map(decode_image, num_parallel_calls=config.AUTOTUNE)\n        .batch(config.BATCH_SIZE)\n        .prefetch(config.AUTOTUNE)\n    )\n    return ds","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-03-24T17:20:00.35258Z","iopub.execute_input":"2025-03-24T17:20:00.352937Z","iopub.status.idle":"2025-03-24T17:20:00.358467Z","shell.execute_reply.started":"2025-03-24T17:20:00.352909Z","shell.execute_reply":"2025-03-24T17:20:00.357653Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# Build dataset from generated PNG paths\npaths = test_df.image_path.tolist()\nds = build_dataset(paths)\n\n# Peek at a batch of images\nimages = next(iter(ds))\nprint(\"Batch shape:\", images.shape)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-03-24T17:20:20.875384Z","iopub.execute_input":"2025-03-24T17:20:20.875679Z","iopub.status.idle":"2025-03-24T17:20:21.017358Z","shell.execute_reply.started":"2025-03-24T17:20:20.875657Z","shell.execute_reply":"2025-03-24T17:20:21.016559Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# Visualize first few images\nkeras_cv.visualization.plot_image_gallery(\n    images=images,\n    value_range=(0, 1),\n    rows=1,\n    cols=3,\n)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-03-24T17:20:21.837456Z","iopub.execute_input":"2025-03-24T17:20:21.837762Z","iopub.status.idle":"2025-03-24T17:20:22.064788Z","shell.execute_reply.started":"2025-03-24T17:20:21.837741Z","shell.execute_reply":"2025-03-24T17:20:22.06395Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"# Inference","metadata":{}},{"cell_type":"code","source":"# Post-processing function to convert raw predictions into multi-class labels\ndef post_proc(pred):\n    proc_pred = np.empty((pred.shape[0], 2*2 + 3*3), dtype=\"float32\")\n\n    # bowel, extravasation\n    proc_pred[:, 0] = pred[:, 0]\n    proc_pred[:, 1] = 1 - proc_pred[:, 0]\n    proc_pred[:, 2] = pred[:, 1]\n    proc_pred[:, 3] = 1 - proc_pred[:, 2]\n    \n    # liver, kidney, spleen\n    proc_pred[:, 4:7] = pred[:, 2:5]\n    proc_pred[:, 7:10] = pred[:, 5:8]\n    proc_pred[:, 10:13] = pred[:, 8:11]\n\n    return proc_pred","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-03-24T17:20:24.813194Z","iopub.execute_input":"2025-03-24T17:20:24.813487Z","iopub.status.idle":"2025-03-24T17:20:24.818523Z","shell.execute_reply.started":"2025-03-24T17:20:24.813465Z","shell.execute_reply":"2025-03-24T17:20:24.81773Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# Unique patient IDs from test data\npatient_ids = test_df[\"patient_id\"].unique()\n\n# Initialize prediction array\npatient_preds = np.zeros(\n    shape=(len(patient_ids), 2*2 + 3*3),  # total of 13 output classes\n    dtype=\"float32\"\n)\n\n# Run inference per patient\nfor pidx, patient_id in tqdm(enumerate(patient_ids), total=len(patient_ids), desc=\"Patients\"):\n    print(f\"Patient ID: {patient_id}\")\n    \n    # Slice dataframe for this patient\n    patient_df = test_df.query(\"patient_id == @patient_id\")\n    patient_paths = patient_df.image_path.tolist()\n\n    # Create dataset and predict\n    dtest = build_dataset(patient_paths)\n    pred = model.predict(dtest)\n    \n    pred = np.concatenate(pred, axis=-1).astype(\"float32\")\n    pred = pred[:len(patient_paths), :]\n    pred = np.mean(pred.reshape(1, len(patient_paths), 11), axis=0)\n    pred = np.max(pred, axis=0, keepdims=True)\n    \n    # Apply post-processing and store\n    patient_preds[pidx, :] += post_proc(pred)[0]\n\n    # Clean up\n    del patient_df, patient_paths, dtest, pred\n    gc.collect()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-03-24T17:20:26.586447Z","iopub.execute_input":"2025-03-24T17:20:26.586815Z","iopub.status.idle":"2025-03-24T17:20:32.25327Z","shell.execute_reply.started":"2025-03-24T17:20:26.586778Z","shell.execute_reply":"2025-03-24T17:20:32.252101Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"# Submission","metadata":{}},{"cell_type":"code","source":"!rm -rf {MODEL_PATH}","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-03-24T17:20:32.254524Z","iopub.execute_input":"2025-03-24T17:20:32.254864Z","iopub.status.idle":"2025-03-24T17:20:32.636481Z","shell.execute_reply.started":"2025-03-24T17:20:32.254816Z","shell.execute_reply":"2025-03-24T17:20:32.635374Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"print(\"patient_preds shape:\", patient_preds.shape)\nprint(\"len(config.TARGET_COLS):\", len(config.TARGET_COLS))\nprint(\"TARGET_COLS:\", config.TARGET_COLS)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-03-24T17:20:36.139713Z","iopub.execute_input":"2025-03-24T17:20:36.140085Z","iopub.status.idle":"2025-03-24T17:20:36.145934Z","shell.execute_reply.started":"2025-03-24T17:20:36.140055Z","shell.execute_reply":"2025-03-24T17:20:36.145145Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# Create Submission\nsubmission_indices = [1, 3, 7, 8, 9, 4, 5, 6, 10, 11, 12]  # select only the official targets\npred_df = pd.DataFrame(patient_preds[:, submission_indices], columns=config.TARGET_COLS)\npred_df.insert(0, \"patient_id\", patient_ids)\n\n# Align with sample submission\nsub_df = pd.read_csv(f\"{BASEPATH}/sample_submission.csv\")\nsub_df = sub_df[[\"patient_id\"]]\nsub_df = sub_df.merge(pred_df, on=\"patient_id\", how=\"left\")\n\n# Store submission\nsub_df.to_csv(\"submission.csv\", index=False)\nsub_df.head(2)\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-03-24T17:20:36.895697Z","iopub.execute_input":"2025-03-24T17:20:36.896002Z","iopub.status.idle":"2025-03-24T17:20:36.932801Z","shell.execute_reply.started":"2025-03-24T17:20:36.895978Z","shell.execute_reply":"2025-03-24T17:20:36.931949Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"","metadata":{"trusted":true},"outputs":[],"execution_count":null}]}