{"metadata":{"kernelspec":{"language":"python","display_name":"Python 3","name":"python3"},"language_info":{"name":"python","version":"3.10.13","mimetype":"text/x-python","codemirror_mode":{"name":"ipython","version":3},"pygments_lexer":"ipython3","nbconvert_exporter":"python","file_extension":".py"},"kaggle":{"accelerator":"nvidiaTeslaT4","dataSources":[{"sourceType":"competition","sourceId":24800,"datasetId":1042002,"databundleVersionId":1831594},{"sourceType":"datasetVersion","sourceId":7842777,"datasetId":4598090,"databundleVersionId":7946975},{"sourceType":"datasetVersion","sourceId":7859360,"datasetId":4610186,"databundleVersionId":7964311},{"sourceType":"datasetVersion","sourceId":7845689,"datasetId":4600233,"databundleVersionId":7950038},{"sourceType":"datasetVersion","sourceId":7832281,"datasetId":4590366,"databundleVersionId":7936067},{"sourceType":"datasetVersion","sourceId":7855309,"datasetId":4607283,"databundleVersionId":7960130},{"sourceType":"datasetVersion","sourceId":7856019,"datasetId":4607735,"databundleVersionId":7960872}],"dockerImageVersionId":30664,"isInternetEnabled":true,"language":"python","sourceType":"notebook","isGpuEnabled":true}},"nbformat_minor":4,"nbformat":4,"cells":[{"cell_type":"code","source":"!pip install ensemble-boxes","metadata":{"execution":{"iopub.status.busy":"2024-03-16T17:45:38.972613Z","iopub.execute_input":"2024-03-16T17:45:38.973251Z","iopub.status.idle":"2024-03-16T17:45:51.355117Z","shell.execute_reply.started":"2024-03-16T17:45:38.973183Z","shell.execute_reply":"2024-03-16T17:45:51.353884Z"},"_kg_hide-output":true,"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"code","source":"# This Python 3 environment comes with many helpful analytics libraries installed\n# It is defined by the kaggle/python Docker image: https://github.com/kaggle/docker-python\n# For example, here's several helpful packages to load\n\nimport numpy as np # linear algebra\nimport pandas as pd # data processing, CSV file I/O (e.g. pd.read_csv)\nimport matplotlib.pyplot as plt\nimport matplotlib.patches as patches\nimport os\nimport pydicom\nfrom PIL import Image\nfrom ensemble_boxes import *\n\n# Input data files are available in the read-only \"../input/\" directory\n# For example, running this (by clicking run or pressing Shift+Enter) will list all files under the input directory\n\n# import os\n# for dirname, _, filenames in os.walk('/kaggle/input'):\n#    for filename in filenames:\n#        print(os.path.join(dirname, filename))\n\n# You can write up to 20GB to the current directory (/kaggle/working/) that gets preserved as output when you create a version using \"Save & Run All\" \n# You can also write temporary files to /kaggle/temp/, but they won't be saved outside of the current session\n\n","metadata":{"_uuid":"8f2839f25d086af736a60e9eeb907d3b93b6e0e5","_cell_guid":"b1076dfc-b9ad-4769-8c92-a6c4dae69d19","execution":{"iopub.status.busy":"2024-03-16T17:45:51.357446Z","iopub.execute_input":"2024-03-16T17:45:51.357778Z","iopub.status.idle":"2024-03-16T17:45:51.364371Z","shell.execute_reply.started":"2024-03-16T17:45:51.357746Z","shell.execute_reply":"2024-03-16T17:45:51.363496Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"markdown","source":"# Chest X-ray Abnormalities Detection\n\n*The released dataset\nis divided into a training set of 15,000 and a test set of 3,000. Each scan in the training set was independently labeled by 3\nradiologists, while each scan in the test set was labeled by the consensus of 5 radiologists. We designed and built a labeling\nplatform for DICOM images to facilitate these annotation procedures. All images are made publicly available in DICOM format\nalong with the labels of both the training set and the test set of the following CSV format:*  \n\nimage_id,class_name,class_id,rad_id,x_min,y_min,x_max,y_max  \n1,No finding,14,R11,,,,  \n2,No finding,14,R7,,,,  \n3,Cardiomegaly,3,R10,691.0,1375.0,1653.0,1831.0  \n3,Cardiomegaly,3,R11,694.0,1370.0,1550.0,1900.0\n\n*Images in the test set may contain more than one object. For each object in a given test image, you must predict a class ID, confidence score, and bounding box in format xmin ymin xmax ymax. If you predict that there are NO objects in a given image, you should predict 14 1.0 0 0 1 1, where 14 is the class ID for \"No finding\", 1.0 is the confidence, and 0 0 1 1 is a one-pixel bounding box.  \nThe submission file should contain a header and have the following format:*\n\nID,TARGET  \n004f33259ee4aef671c2b95d54e4be68,14 1 0 0 1 1  \n004f33259ee4aef671c2b95d54e4be69,11 0.5 100 100 200 200 13 0.7 10 10 20 20  \netc.\n\n\n**Approach**\n\nTo solve this problem, a 3 steps approach will be taken :\n\n* Data analysis & preprocessing\n* ML model training (YOLOV8)\n* Validation\n\n# **Data analysis & preprocessing**","metadata":{}},{"cell_type":"code","source":"# Paths to the dicom directories\nTEST_DIR = (\"/kaggle/input/vinbigdata-chest-xray-abnormalities-detection/test\")\nTRAIN_DIR = (\"/kaggle/input/vinbigdata-chest-xray-abnormalities-detection/train\")\n\n# Paths to each individual files in the dicom directories\nTEST_DICOM_PATHS = [os.path.join(TEST_DIR, f_name) for f_name in os.listdir(TEST_DIR)]\nTRAIN_DICOM_PATHS = [os.path.join(TRAIN_DIR, f_name) for f_name in os.listdir(TRAIN_DIR)]\nprint(f\"\\n {len(TRAIN_DICOM_PATHS)} training files...\")\nprint(f\" {len(TEST_DICOM_PATHS)} test files...\")\n\n# Define paths to the relevant csv files\nTRAIN_CSV = (\"/kaggle/input/vinbigdata-chest-xray-abnormalities-detection/train.csv\")\nSS_CSV = (\"/kaggle/input/vinbigdata-chest-xray-abnormalities-detection/sample_submission.csv\")\n\n# Create pandas dataframes \ntrain_df = pd.read_csv(TRAIN_CSV)\nsub_df = pd.read_csv(SS_CSV)\n\nprint(\"\\n\\nTRAIN DATAFRAME\\n\\n\")\ndisplay(train_df.head(4))\n\nprint(\"\\n\\nSAMPLE SUBMISSION DATAFRAME\\n\\n\")\ndisplay(sub_df.head(4))","metadata":{"execution":{"iopub.status.busy":"2024-03-16T19:19:29.507978Z","iopub.execute_input":"2024-03-16T19:19:29.508381Z","iopub.status.idle":"2024-03-16T19:19:29.68364Z","shell.execute_reply.started":"2024-03-16T19:19:29.508352Z","shell.execute_reply":"2024-03-16T19:19:29.682673Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"code","source":"train_df.query(\"image_id == '50a418190bc3fb1ef1633bf9678929b3'\")","metadata":{"execution":{"iopub.status.busy":"2024-03-16T17:45:51.547479Z","iopub.execute_input":"2024-03-16T17:45:51.547792Z","iopub.status.idle":"2024-03-16T17:45:51.570247Z","shell.execute_reply.started":"2024-03-16T17:45:51.547766Z","shell.execute_reply":"2024-03-16T17:45:51.569264Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"markdown","source":"We can see three radiologists are to label the same image.","metadata":{}},{"cell_type":"code","source":"fig, ax = plt.subplots(1, 1, figsize=(12, 4))\nx = train_df['class_name'].value_counts().keys()\ny = train_df['class_name'].value_counts().values\nax.bar(x, y)\nax.set_xticklabels(x, rotation=90)\nax.set_title('Distribution of the labels')\nplt.show()","metadata":{"execution":{"iopub.status.busy":"2024-03-16T17:45:51.571585Z","iopub.execute_input":"2024-03-16T17:45:51.57192Z","iopub.status.idle":"2024-03-16T17:45:51.944378Z","shell.execute_reply.started":"2024-03-16T17:45:51.571889Z","shell.execute_reply":"2024-03-16T17:45:51.943468Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"markdown","source":"We need to be careful, as since multiple radiologists can label the same image, one image can be labelled with \"no finding\" and with an abnormality at the same time.","metadata":{}},{"cell_type":"code","source":"# Step 1: Aggregate the data\ngrouped_df = train_df.groupby(['rad_id', 'class_name']).size().unstack(fill_value=0)\n\n# Sort radiologists by total counts\ngrouped_df['total'] = grouped_df.sum(axis=1)\ngrouped_df = grouped_df.sort_values('total', ascending=False)\nradiologists = grouped_df.index\nlabels = grouped_df.columns.drop('total') # Exclude 'total' from labels for plotting\n\n# Ensure \"No finding\" is plotted last\nlabels = [label for label in grouped_df.columns if label not in ['total', 'No finding']]\nif 'No finding' in grouped_df.columns:\n    labels.append('No finding')  # This makes sure \"No finding\" is plotted last\n\n# Use a color map with better separation\ncolor_map = plt.get_cmap('tab20')\nlabel_colors = color_map(np.linspace(0, 1, len(labels)))\n\n# Plotting\nfig, ax = plt.subplots(figsize=(12, 4))\n\nbottom = np.zeros(len(radiologists))\nfor i, label in enumerate(labels):\n    ax.bar(radiologists, grouped_df[label], bottom=bottom, label=label, color=label_colors[i])\n    bottom += grouped_df[label].values\n\nax.set_title('Number of Annotations per Radiologist with Label Distribution')\n\n# Move legend outside the plot on the right\nplt.legend(title='Labels', bbox_to_anchor=(1.05, 1), loc='upper left')\n\nplt.tight_layout() # Adjust layout to make room for the legend\nplt.show()","metadata":{"execution":{"iopub.status.busy":"2024-03-16T17:45:51.945682Z","iopub.execute_input":"2024-03-16T17:45:51.946323Z","iopub.status.idle":"2024-03-16T17:45:52.89357Z","shell.execute_reply.started":"2024-03-16T17:45:51.946288Z","shell.execute_reply":"2024-03-16T17:45:52.892617Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"markdown","source":"We observe that three radiologists are responsible for most of the labelling, especially for the abnormalities detected.\nIt looks like the other radiologists are heavily skewed towards \"no finding\". ","metadata":{}},{"cell_type":"code","source":"is_normal_df = train_df.groupby(\"image_id\")[\"class_id\"].agg(lambda s: (s == 14).sum()).reset_index().rename({\"class_id\": \"num_normal_labels\"}, axis=1)\nis_normal_df.head()","metadata":{"execution":{"iopub.status.busy":"2024-03-16T17:45:52.894771Z","iopub.execute_input":"2024-03-16T17:45:52.89507Z","iopub.status.idle":"2024-03-16T17:45:54.633101Z","shell.execute_reply.started":"2024-03-16T17:45:52.895045Z","shell.execute_reply":"2024-03-16T17:45:54.632148Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"code","source":"num_normal_anno_counts = is_normal_df[\"num_normal_labels\"].value_counts()\nnum_normal_anno_counts.plot(kind=\"bar\")\nplt.title(\"The number of 'No finding' labels in each image\")","metadata":{"execution":{"iopub.status.busy":"2024-03-16T17:45:54.634334Z","iopub.execute_input":"2024-03-16T17:45:54.634677Z","iopub.status.idle":"2024-03-16T17:45:54.889963Z","shell.execute_reply.started":"2024-03-16T17:45:54.634652Z","shell.execute_reply":"2024-03-16T17:45:54.888929Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"markdown","source":"We know that each image is annoted by exactly 3 radiologists. This plot shows that for all images, all 3 radiologists agree on the presence or not of an abnormality. \n\nHowever, not all 3 radiologists agree on the nature of the abnornality.","metadata":{}},{"cell_type":"code","source":"is_normal_df = train_df.groupby(\"image_id\")[\"class_id\"].agg(lambda s: (s == 0).sum()).reset_index().rename({\"class_id\": \"num_ao_labels\"}, axis=1)\nnum_normal_anno_counts = is_normal_df[\"num_ao_labels\"].value_counts()\nnum_normal_anno_counts.plot(kind=\"bar\")\nplt.title(\"The number of 'Aortic enlargement' labels in each image\")","metadata":{"execution":{"iopub.status.busy":"2024-03-16T17:45:54.891179Z","iopub.execute_input":"2024-03-16T17:45:54.891553Z","iopub.status.idle":"2024-03-16T17:45:56.88135Z","shell.execute_reply.started":"2024-03-16T17:45:54.89152Z","shell.execute_reply":"2024-03-16T17:45:56.880384Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"markdown","source":"We can only conclude that only 1/3rd of the images present abnormalities.  \nThis is key for our strategy concerning the prediction of class 14, or the classification of normal/abnormal images, or again the non detection of abnormailities.  \nWe can imagine different strategies:\n* Two-stage prediction by training a binary classification model that decides whether any anomaly is present or not in the X-ray image, and an object detection model to detect what abnormalities are present on the image.\n* Training an object detection model with \"No finding\" as a class, and during inference, if the model's highest confidence prediction is for \"No finding\", output class 14 with a dummy bounding box (e.g., 0 0 1 1).\n* Train an object detection model that detects abnormalities. After preditcion, if the confidence level (total or for each abnormality) is below a certain treshold, classify the image as \"No finding\".\n\nAlternatively, we can create models for each of these strategies, compare them and possibly \"Ensemble\" them.\n\nMoreover, since each picture is annoted by 3 radiologists, and knowing that these 3 radiologists only always agree on class 14 (No finding), we need to keep in mind how many of the three radiologists agree on the nature of the disease regarding the object detection for the prediction of the abnormalities.  \nThis gives us three different datasets to study.\n* One dataset 1. where we leave the boxes judged an abnormality by 1 or more radiologists.\n* One dataset 2. where we leave the boxes judged an abnormality by 2 or more radiologists.\n* One dataset 3. where we leave the boxes judged an abnormality by all 3 radiologist.\n\nBy training one or multiple models per dataset and then ensembling them, the boxes judged an abnormaloty by more radiologists will have a higher confidence score. For example, if one box is judged a disease by all three radiologists, it will detected thrice (by the three models). This is why merging the boxes using wbf is key.\n\nNow that we know the datasets we are going to use, here is my plan on the training of the models, and then the ensembling :\n\n* Each dataset will give two models : each dataset will be trained on both all the 15 000 training images for the first model (x.1), and on only the images presenting abnormalities for the second model (x.2). This will give us dataset 1.1, 1.2, 2.1, 2.2, 3.1, 3.2\n* This will give us 6 different models, and a lot of different configurations for ensembling.\n* For the ensembling, fusing models 1.1, 2.2, and 3.2 looks the most promising to be as it reduces the class imbalances between class_id = 14 : no finding and the other classes.\n\n","metadata":{}},{"cell_type":"markdown","source":"**DICOM format analysis**","metadata":{}},{"cell_type":"code","source":"import pydicom\n    \ndef analyze_dicom_image(path):\n    # Load the DICOM file\n    dicom = pydicom.dcmread(path)\n    \n    # Extract the dimensions (size)\n    width, height = dicom.pixel_array.shape[1], dicom.pixel_array.shape[0]\n    print(f\"Width: {width} pixels, Height: {height} pixels\")\n    \n    # Extract pixel spacing\n    pixel_spacing = dicom.PixelSpacing if 'PixelSpacing' in dicom else \"Unknown\"\n    print(f\"Pixel Spacing: {pixel_spacing} mm\")\n\n    # Extract Window Width and Window Center if available\n    window_width = dicom.WindowWidth if 'WindowWidth' in dicom else \"Unknown\"\n    window_center = dicom.WindowCenter if 'WindowCenter' in dicom else \"Unknown\"\n    print(f\"Window Width: {window_width}, Window Center: {window_center}\")\n","metadata":{"execution":{"iopub.status.busy":"2024-03-16T17:45:56.885894Z","iopub.execute_input":"2024-03-16T17:45:56.886205Z","iopub.status.idle":"2024-03-16T17:45:56.894279Z","shell.execute_reply.started":"2024-03-16T17:45:56.886177Z","shell.execute_reply":"2024-03-16T17:45:56.893237Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"code","source":"# Analyze the first DICOM images in the training directory\nfor path in TRAIN_DICOM_PATHS[:3]:\n    print(f\"Analyzing: {path}\")\n    analyze_dicom_image(path)\n    print(\"-------------------------------------------------\")","metadata":{"execution":{"iopub.status.busy":"2024-03-16T17:45:56.8955Z","iopub.execute_input":"2024-03-16T17:45:56.895845Z","iopub.status.idle":"2024-03-16T17:45:59.823867Z","shell.execute_reply.started":"2024-03-16T17:45:56.895814Z","shell.execute_reply":"2024-03-16T17:45:59.821019Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"markdown","source":"**Image transformation**  \n\nAs we can see, the images in dicom format have varying formats. When training our object detection model, having the same format for each image is important to get better results. While doing that, we need to make sure to preserve the correct aspect ratio (use of padding).\n\nSee https://www.kaggle.com/code/gabrielmaire/image-transformation for the image conversion code. This notebook outputs a folder holding each transformed image of the train set.\n\nFor my case, space and size is a priority as there is a close deadline and my computer as well as my kaggle account does not have a lot of space available. That is I chose 640x640 jpg format for the conversion.","metadata":{}},{"cell_type":"markdown","source":"**BBox adjustment**  \n\nThe images that are going to be used for training are now in a new format where they have been resized and padded to keep the original aspect ratio.  \nThe boundary boxes coordinates must be adjusted according to this resizing.  \n\nTo do that, we first get the original sizes of the images.  \nSee https://www.kaggle.com/code/gabrielmaire/original-sizes-csv for the code to get the original sizes of the images. This notebook outputs a csv file holding the original dimensions of each image for both the train and test set.","metadata":{}},{"cell_type":"code","source":"original_dims_csv = (\"/kaggle/input/images-original-sizes/train_original_dimensions.csv\")\noriginal_dims_df = pd.read_csv(original_dims_csv)\n\n# Convert to dictionary mapping image_id to a tuple of (width, height)\noriginal_dimensions = {row['image_id']: (row['width'], row['height']) for index, row in original_dims_df.iterrows()}","metadata":{"execution":{"iopub.status.busy":"2024-03-16T17:45:59.825314Z","iopub.execute_input":"2024-03-16T17:45:59.82573Z","iopub.status.idle":"2024-03-16T17:46:00.77701Z","shell.execute_reply.started":"2024-03-16T17:45:59.825691Z","shell.execute_reply":"2024-03-16T17:46:00.776206Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"code","source":"# Adjusts the bbox from original dimensions to 640x640, taking into account the padding to save aspect ratio.\ndef adjust_bbox(row, dimensions_dict, desired_size=(640, 640)):\n    \n    if row['class_id'] == 14:\n        return row  # Skip adjustment for class_id 14\n    \n    # Retrieve the original dimensions using the image ID from the row\n    original_width, original_height = dimensions_dict[row['image_id']]\n    \n    # Calculate the ratio used for resizing while maintaining aspect ratio\n    ratio = min(desired_size[0] / original_width, desired_size[1] / original_height)\n    \n    # Calculate new bounding box coordinates, scaled according to the ratio\n    row['x_min'] = int(row['x_min'] * ratio)\n    row['y_min'] = int(row['y_min'] * ratio)\n    row['x_max'] = int(row['x_max'] * ratio)\n    row['y_max'] = int(row['y_max'] * ratio)\n    \n    # Calculate padding offsets based on the new size after resizing\n    new_width = int(original_width * ratio)\n    new_height = int(original_height * ratio)\n    pad_x = (desired_size[0] - new_width) // 2\n    pad_y = (desired_size[1] - new_height) // 2\n    \n    # Adjust coordinates to account for padding added to maintain aspect ratio\n    row['x_min'] += pad_x\n    row['x_max'] += pad_x\n    row['y_min'] += pad_y\n    row['y_max'] += pad_y\n    \n    return row","metadata":{"execution":{"iopub.status.busy":"2024-03-16T17:46:00.778109Z","iopub.execute_input":"2024-03-16T17:46:00.778404Z","iopub.status.idle":"2024-03-16T17:46:00.786906Z","shell.execute_reply.started":"2024-03-16T17:46:00.778379Z","shell.execute_reply":"2024-03-16T17:46:00.785943Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"code","source":"# Adjusting bounding box coordinates for each row in the DataFrame\ntraindf_640 = train_df.apply(lambda row: adjust_bbox(row, original_dimensions), axis=1)","metadata":{"execution":{"iopub.status.busy":"2024-03-16T17:46:00.787969Z","iopub.execute_input":"2024-03-16T17:46:00.788257Z","iopub.status.idle":"2024-03-16T17:46:13.468131Z","shell.execute_reply.started":"2024-03-16T17:46:00.788234Z","shell.execute_reply":"2024-03-16T17:46:13.467012Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"code","source":"# Comparison\ntrain_df.head(5)","metadata":{"execution":{"iopub.status.busy":"2024-03-16T17:46:13.470172Z","iopub.execute_input":"2024-03-16T17:46:13.47053Z","iopub.status.idle":"2024-03-16T17:46:13.486112Z","shell.execute_reply.started":"2024-03-16T17:46:13.470501Z","shell.execute_reply":"2024-03-16T17:46:13.485139Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"code","source":"traindf_640.head(5)","metadata":{"execution":{"iopub.status.busy":"2024-03-16T17:46:13.487065Z","iopub.execute_input":"2024-03-16T17:46:13.487317Z","iopub.status.idle":"2024-03-16T17:46:13.505175Z","shell.execute_reply.started":"2024-03-16T17:46:13.487294Z","shell.execute_reply":"2024-03-16T17:46:13.504234Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"markdown","source":"It looks like the coordinates have been correctly adjusted. However, drawing the bboxes on the images will give us a better validation.","metadata":{}},{"cell_type":"code","source":"# Different colour bounding boxes in function of the class\nclass_id_to_color = {\n    0: 'red', 1: 'green', 2: 'blue', 3: 'cyan', 4: 'magenta',\n    5: 'yellow', 6: 'gold', 7: 'orange', 8: 'purple', 9: 'brown',\n    10: 'pink', 11: 'lime', 12: 'olive', 13: 'indigo'\n}","metadata":{"execution":{"iopub.status.busy":"2024-03-16T17:46:13.506374Z","iopub.execute_input":"2024-03-16T17:46:13.506764Z","iopub.status.idle":"2024-03-16T17:46:13.513311Z","shell.execute_reply.started":"2024-03-16T17:46:13.50673Z","shell.execute_reply":"2024-03-16T17:46:13.51247Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"code","source":"# Function to display the images with the bounding boxes\n\ndef display_image_with_boxes(image_id, image_folder, annotations_df):\n    # Filter the annotations for the given image_id\n    image_annotations = annotations_df[annotations_df['image_id'] == image_id]\n\n    # Load the image\n    image_path = f'{image_folder}/{image_id}.jpg'  # Update the extension if needed\n    image = Image.open(image_path)\n\n    # Create a figure and axis for plotting\n    fig, ax = plt.subplots(1)\n    ax.imshow(image)\n\n    # Draw each bounding box and write the class name\n    for _, row in image_annotations.iterrows():\n        if row['class_id'] == 14:  # Skip if class_id is 14 (no finding)\n            continue\n        x_min, y_min, x_max, y_max, class_id = row['x_min'], row['y_min'], row['x_max'], row['y_max'], row['class_id']\n        class_name = row['class_name']\n        color = class_id_to_color.get(class_id, 'white')  # Use white as default color if class_id is not in the map\n        \n        # Draw rectangle\n        rect = patches.Rectangle((x_min, y_min), x_max - x_min, y_max - y_min, linewidth=2, edgecolor=color, facecolor='none', alpha=0.5)\n        ax.add_patch(rect)\n        \n        # Write class name\n        ax.text(x_min, y_min - 5, class_name, color=color, fontsize=6, verticalalignment='bottom', bbox=dict(facecolor='none', alpha=0.2, edgecolor='none'))\n\n    plt.show()","metadata":{"execution":{"iopub.status.busy":"2024-03-16T17:46:13.514589Z","iopub.execute_input":"2024-03-16T17:46:13.514895Z","iopub.status.idle":"2024-03-16T17:46:13.524896Z","shell.execute_reply.started":"2024-03-16T17:46:13.51487Z","shell.execute_reply":"2024-03-16T17:46:13.523805Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"code","source":"# Specify the image ID and folder containing your 640x640 images\nimage_id = \"9a5094b2563a1ef3ff50dc5c7ff71345\"\nTRAIN_640 = '/kaggle/input/train-dir-640x640/TRAIN_DIR_JPG_640x640'\nTRAIN_JPG = '/kaggle/input/train-dir-jpg-1/TRAIN_DIR_JPG'\n\ndisplay_image_with_boxes(image_id, TRAIN_JPG, train_df) # Original size img and annotations\ndisplay_image_with_boxes(image_id, TRAIN_640, traindf_640) # Converted img and annotations\n","metadata":{"execution":{"iopub.status.busy":"2024-03-16T17:46:13.525958Z","iopub.execute_input":"2024-03-16T17:46:13.526238Z","iopub.status.idle":"2024-03-16T17:46:14.476299Z","shell.execute_reply.started":"2024-03-16T17:46:13.526214Z","shell.execute_reply":"2024-03-16T17:46:14.475359Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"markdown","source":"The first image (converted image) is indeed 640x640, keeps the correct aspect ratio with the use of padding, and the boundary boxes coordinates have been correctly adjusted.","metadata":{}},{"cell_type":"markdown","source":"# **Weighted Box Fusion (WBF)**\n\nWe apply weighted box fusion on images where class 14 != 0. That is to say on images where abnormalities have been detected.  \nIt is important to keep in mind that the annotations made by the radiologists do not have a confidence score.\n\nFor my future training (ensembling of 3 different models based on confidence levels given how many radiologists annotated an abnormality), I want to keep track of how many radiologists annotated roughly the same area for the same disease with a boundary box.  \nApplying WBF helps fusing bboxes in the same area, but it loses track of which annotators - in this case the radiologists - annotated which boundary boxes.  \nTherefore, I modified the source code of the weighted_boxes_fusion function.  \nThis modified function adds a variable counts that keeps track of how many boundary boxes have been fused for each fused boundary box for each image.  \nThis is equivalent to how many radiologists annoted the boundary boxes that have been fused together.\n","metadata":{}},{"cell_type":"code","source":"# Modified WBF code to also keep track of the number of bboxes fused together for each bbox fusion. \n# This is important to know how many annotators agree on an abnormality\n\n# coding: utf-8\n__author__ = 'ZFTurbo: https://kaggle.com/zfturbo'\n__Modification__ = 'Gabriel Maire'\n\ndef prefilter_boxes(boxes, scores, labels, weights, thr):\n    # Create dict with boxes stored by its label\n    new_boxes = dict()\n\n    for t in range(len(boxes)):\n\n        if len(boxes[t]) != len(scores[t]):\n            print('Error. Length of boxes arrays not equal to length of scores array: {} != {}'.format(len(boxes[t]), len(scores[t])))\n            exit()\n\n        if len(boxes[t]) != len(labels[t]):\n            print('Error. Length of boxes arrays not equal to length of labels array: {} != {}'.format(len(boxes[t]), len(labels[t])))\n            exit()\n\n        for j in range(len(boxes[t])):\n            score = scores[t][j]\n            if score < thr:\n                continue\n            label = int(labels[t][j])\n            box_part = boxes[t][j]\n            x1 = float(box_part[0])\n            y1 = float(box_part[1])\n            x2 = float(box_part[2])\n            y2 = float(box_part[3])\n\n            # Box data checks\n            if x2 < x1:\n                warnings.warn('X2 < X1 value in box. Swap them.')\n                x1, x2 = x2, x1\n            if y2 < y1:\n                warnings.warn('Y2 < Y1 value in box. Swap them.')\n                y1, y2 = y2, y1\n            if x1 < 0:\n                warnings.warn('X1 < 0 in box. Set it to 0.')\n                x1 = 0\n            if x1 > 1:\n                warnings.warn('X1 > 1 in box. Set it to 1. Check that you normalize boxes in [0, 1] range.')\n                x1 = 1\n            if x2 < 0:\n                warnings.warn('X2 < 0 in box. Set it to 0.')\n                x2 = 0\n            if x2 > 1:\n                warnings.warn('X2 > 1 in box. Set it to 1. Check that you normalize boxes in [0, 1] range.')\n                x2 = 1\n            if y1 < 0:\n                warnings.warn('Y1 < 0 in box. Set it to 0.')\n                y1 = 0\n            if y1 > 1:\n                warnings.warn('Y1 > 1 in box. Set it to 1. Check that you normalize boxes in [0, 1] range.')\n                y1 = 1\n            if y2 < 0:\n                warnings.warn('Y2 < 0 in box. Set it to 0.')\n                y2 = 0\n            if y2 > 1:\n                warnings.warn('Y2 > 1 in box. Set it to 1. Check that you normalize boxes in [0, 1] range.')\n                y2 = 1\n            if (x2 - x1) * (y2 - y1) == 0.0:\n                warnings.warn(\"Zero area box skipped: {}.\".format(box_part))\n                continue\n\n            # [label, score, weight, model index, x1, y1, x2, y2]\n            b = [int(label), float(score) * weights[t], weights[t], t, x1, y1, x2, y2]\n            if label not in new_boxes:\n                new_boxes[label] = []\n            new_boxes[label].append(b)\n\n    # Sort each list in dict by score and transform it to numpy array\n    for k in new_boxes:\n        current_boxes = np.array(new_boxes[k])\n        new_boxes[k] = current_boxes[current_boxes[:, 1].argsort()[::-1]]\n\n    return new_boxes\n\n\ndef get_weighted_box(boxes, conf_type='avg'):\n    \"\"\"\n    Create weighted box for set of boxes\n    :param boxes: set of boxes to fuse\n    :param conf_type: type of confidence one of 'avg' or 'max'\n    :return: weighted box (label, score, weight, model index, x1, y1, x2, y2)\n    \"\"\"\n\n    box = np.zeros(8, dtype=np.float32)\n    conf = 0\n    conf_list = []\n    w = 0\n    for b in boxes:\n        box[4:] += (b[1] * b[4:])\n        conf += b[1]\n        conf_list.append(b[1])\n        w += b[2]\n    box[0] = boxes[0][0]\n    if conf_type in ('avg', 'box_and_model_avg', 'absent_model_aware_avg'):\n        box[1] = conf / len(boxes)\n    elif conf_type == 'max':\n        box[1] = np.array(conf_list).max()\n    box[2] = w\n    box[3] = -1 # model index field is retained for consistency but is not used.\n    box[4:] /= conf\n    return box\n\n\ndef find_matching_box_fast(boxes_list, new_box, match_iou):\n    \"\"\"\n        Reimplementation of find_matching_box with numpy instead of loops. Gives significant speed up for larger arrays\n        (~100x). This was previously the bottleneck since the function is called for every entry in the array.\n    \"\"\"\n    def bb_iou_array(boxes, new_box):\n        # bb interesection over union\n        xA = np.maximum(boxes[:, 0], new_box[0])\n        yA = np.maximum(boxes[:, 1], new_box[1])\n        xB = np.minimum(boxes[:, 2], new_box[2])\n        yB = np.minimum(boxes[:, 3], new_box[3])\n\n        interArea = np.maximum(xB - xA, 0) * np.maximum(yB - yA, 0)\n\n        # compute the area of both the prediction and ground-truth rectangles\n        boxAArea = (boxes[:, 2] - boxes[:, 0]) * (boxes[:, 3] - boxes[:, 1])\n        boxBArea = (new_box[2] - new_box[0]) * (new_box[3] - new_box[1])\n\n        iou = interArea / (boxAArea + boxBArea - interArea)\n\n        return iou\n\n    if boxes_list.shape[0] == 0:\n        return -1, match_iou\n\n    # boxes = np.array(boxes_list)\n    boxes = boxes_list\n\n    ious = bb_iou_array(boxes[:, 4:], new_box[4:])\n\n    ious[boxes[:, 0] != new_box[0]] = -1\n\n    best_idx = np.argmax(ious)\n    best_iou = ious[best_idx]\n\n    if best_iou <= match_iou:\n        best_iou = match_iou\n        best_idx = -1\n\n    return best_idx, best_iou\n\n\ndef weighted_boxes_fusion(\n        boxes_list,\n        scores_list,\n        labels_list,\n        weights=None,\n        iou_thr=0.55,\n        skip_box_thr=0.0,\n        conf_type='avg',\n        allows_overflow=False\n):\n    '''\n    :param boxes_list: list of boxes predictions from each model, each box is 4 numbers.\n    It has 3 dimensions (models_number, model_preds, 4)\n    Order of boxes: x1, y1, x2, y2. We expect float normalized coordinates [0; 1]\n    :param scores_list: list of scores for each model\n    :param labels_list: list of labels for each model\n    :param weights: list of weights for each model. Default: None, which means weight == 1 for each model\n    :param iou_thr: IoU value for boxes to be a match\n    :param skip_box_thr: exclude boxes with score lower than this variable\n    :param conf_type: how to calculate confidence in weighted boxes.\n        'avg': average value,\n        'max': maximum value,\n        'box_and_model_avg': box and model wise hybrid weighted average,\n        'absent_model_aware_avg': weighted average that takes into account the absent model.\n    :param allows_overflow: false if we want confidence score not exceed 1.0\n\n    :return: boxes: boxes coordinates (Order of boxes: x1, y1, x2, y2).\n    :return: scores: confidence scores\n    :return: labels: boxes labels\n    '''\n\n    if weights is None:\n        weights = np.ones(len(boxes_list))\n    if len(weights) != len(boxes_list):\n        print('Warning: incorrect number of weights {}. Must be: {}. Set weights equal to 1.'.format(len(weights), len(boxes_list)))\n        weights = np.ones(len(boxes_list))\n    weights = np.array(weights)\n\n    if conf_type not in ['avg', 'max', 'box_and_model_avg', 'absent_model_aware_avg']:\n        print('Unknown conf_type: {}. Must be \"avg\", \"max\" or \"box_and_model_avg\", or \"absent_model_aware_avg\"'.format(conf_type))\n        exit()\n\n    filtered_boxes = prefilter_boxes(boxes_list, scores_list, labels_list, weights, skip_box_thr)\n    if len(filtered_boxes) == 0:\n        return np.zeros((0, 4)), np.zeros((0,)), np.zeros((0,)), np.zeros((0,))  # Also return an empty array for counts\n    \n    overall_boxes = []\n    counts = []  # Initialize an empty list to track the count of fused boxes\n\n    for label in filtered_boxes:\n        boxes = filtered_boxes[label]\n        new_boxes = []\n        weighted_boxes = np.empty((0, 9))\n\n        # Clusterize boxes\n        for j in range(0, len(boxes)):\n            index, best_iou = find_matching_box_fast(weighted_boxes, boxes[j], iou_thr)\n\n            if index != -1:\n                new_boxes[index].append(boxes[j])\n                weighted_box = get_weighted_box(new_boxes[index], conf_type)\n                weighted_boxes[index, :8] = weighted_box  # Update weighted box information\n                weighted_boxes[index, 8] += 1  # Increment the count for this fused box\n            else:\n                new_boxes.append([boxes[j].copy()])\n                weighted_boxes = np.vstack((weighted_boxes, np.append(boxes[j].copy(), 1)))  # Append with initial count 1\n\n\n        # Rescale confidence based on number of models and boxes\n        for i in range(len(new_boxes)):\n            clustered_boxes = new_boxes[i]\n            if conf_type == 'box_and_model_avg':\n                clustered_boxes = np.array(clustered_boxes)\n                # weighted average for boxes\n                weighted_boxes[i, 1] = weighted_boxes[i, 1] * len(clustered_boxes) / weighted_boxes[i, 2]\n                # identify unique model index by model index column\n                _, idx = np.unique(clustered_boxes[:, 3], return_index=True)\n                # rescale by unique model weights\n                weighted_boxes[i, 1] = weighted_boxes[i, 1] *  clustered_boxes[idx, 2].sum() / weights.sum()\n            elif conf_type == 'absent_model_aware_avg':\n                clustered_boxes = np.array(clustered_boxes)\n                # get unique model index in the cluster\n                models = np.unique(clustered_boxes[:, 3]).astype(int)\n                # create a mask to get unused model weights\n                mask = np.ones(len(weights), dtype=bool)\n                mask[models] = False\n                # absent model aware weighted average\n                weighted_boxes[i, 1] = weighted_boxes[i, 1] * len(clustered_boxes) / (weighted_boxes[i, 2] + weights[mask].sum())\n            elif conf_type == 'max':\n                weighted_boxes[i, 1] = weighted_boxes[i, 1] / weights.max()\n            elif not allows_overflow:\n                weighted_boxes[i, 1] = weighted_boxes[i, 1] * min(len(weights), len(clustered_boxes)) / weights.sum()\n            else:\n                weighted_boxes[i, 1] = weighted_boxes[i, 1] * len(clustered_boxes) / weights.sum()\n\n#        overall_boxes.append(weighted_boxes[:, :8])  # Update to include only box information without counts\n        overall_boxes.append(weighted_boxes)\n        counts.extend(weighted_boxes[:, 8])  # Extend the counts list with the counts from the current label\n    \n    overall_boxes = np.concatenate(overall_boxes, axis=0)\n    overall_boxes = overall_boxes[overall_boxes[:, 1].argsort()[::-1]]  # Sorting if needed\n    boxes = overall_boxes[:, 4:8]\n    scores = overall_boxes[:, 1]\n    labels = overall_boxes[:, 0]\n#    counts = np.array(counts)  # This should directly come from the same ordering logic as boxes, scores, and labels\n    counts = overall_boxes[:, 8]  # Extract counts aligned with the sorted order\n\n    return boxes, scores, labels, counts","metadata":{"execution":{"iopub.status.busy":"2024-03-16T17:46:14.477747Z","iopub.execute_input":"2024-03-16T17:46:14.478047Z","iopub.status.idle":"2024-03-16T17:46:14.522053Z","shell.execute_reply.started":"2024-03-16T17:46:14.478022Z","shell.execute_reply":"2024-03-16T17:46:14.521042Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"code","source":"#from ensemble_boxes import *\n\n# Function to apply WBF to a dataframe, resulting in the fusion of rows having a boundary box for a certain class close to each other (depending on iou_thr)\n# The new row has averaged coordinates for the new bbox given the coordinates of the fused bboxes\n\ndef apply_wbf_to_dataframe(annotations_df, image_size=640, iou_thr=0.5, skip_box_thr=0.0001):\n    wbf_results = []\n\n    for image_id, group in annotations_df.groupby('image_id'):\n        boxes = group[['x_min', 'y_min', 'x_max', 'y_max']].values / image_size  # Normalize\n        scores = [1.0] * len(group)  # Assuming equal confidence for simplicity\n        labels = group['class_id'].values\n\n        # Apply WBF\n        boxes, scores, labels, counts = weighted_boxes_fusion(\n            [boxes], [scores], [labels], weights=None, iou_thr=iou_thr, skip_box_thr=skip_box_thr\n        )\n\n        # Denormalize boxes\n        boxes = boxes * image_size\n        \n        # Ensure correct alignment by iterating over all elements together\n        for box, score, label, count in zip(boxes, scores, labels, counts):\n            x_min, y_min, x_max, y_max = box\n            result = {\n                'image_id': image_id, \n                'x_min': x_min, 'y_min': y_min, 'x_max': x_max, 'y_max': y_max, \n                'score': score, 'class_id': label,\n                'count': count \n            }\n            wbf_results.append(result)\n\n    return pd.DataFrame(wbf_results)\n","metadata":{"execution":{"iopub.status.busy":"2024-03-16T17:46:14.523388Z","iopub.execute_input":"2024-03-16T17:46:14.52384Z","iopub.status.idle":"2024-03-16T17:46:14.538947Z","shell.execute_reply.started":"2024-03-16T17:46:14.523807Z","shell.execute_reply":"2024-03-16T17:46:14.537853Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"code","source":"# Filter dataframe to only keep bboxes containing abnormalities\ntraindf_640_filtered = traindf_640[traindf_640['class_id'] != 14]\n\n# Apply WBF to the filtered annotations and save the results\nwbf_640 = apply_wbf_to_dataframe(traindf_640_filtered)","metadata":{"execution":{"iopub.status.busy":"2024-03-16T17:46:14.540217Z","iopub.execute_input":"2024-03-16T17:46:14.540608Z","iopub.status.idle":"2024-03-16T17:46:20.184275Z","shell.execute_reply.started":"2024-03-16T17:46:14.540572Z","shell.execute_reply":"2024-03-16T17:46:20.183306Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"code","source":"class_id_to_name = {\n    0.0: \"Aortic enlargement\",\n    1.0: \"Atelectasis\",\n    2.0: \"Calcification\",\n    3.0: \"Cardiomegaly\",\n    4.0: \"Consolidation\",\n    5.0: \"ILD\",\n    6.0: \"Infiltration\",\n    7.0: \"Lung Opacity\",\n    8.0: \"Nodule/Mass\",\n    9.0: \"Other lesion\",\n    10.0: \"Pleural effusion\",\n    11.0: \"Pleural thickening\",\n    12.0: \"Pneumothorax\",\n    13.0: \"Pulmonary fibrosis\",\n    14.0: \"No finding\"\n}","metadata":{"execution":{"iopub.status.busy":"2024-03-16T17:46:20.185682Z","iopub.execute_input":"2024-03-16T17:46:20.186155Z","iopub.status.idle":"2024-03-16T17:46:20.192309Z","shell.execute_reply.started":"2024-03-16T17:46:20.186119Z","shell.execute_reply":"2024-03-16T17:46:20.19127Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"code","source":"# Add the class_name column based on the class_id\nwbf_640['class_name'] = wbf_640['class_id'].map(class_id_to_name)","metadata":{"execution":{"iopub.status.busy":"2024-03-16T17:46:20.1936Z","iopub.execute_input":"2024-03-16T17:46:20.193881Z","iopub.status.idle":"2024-03-16T17:46:20.207527Z","shell.execute_reply.started":"2024-03-16T17:46:20.193849Z","shell.execute_reply":"2024-03-16T17:46:20.206615Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"code","source":"wbf_640.query(f\"image_id == '{image_id}'\")","metadata":{"execution":{"iopub.status.busy":"2024-03-16T17:46:20.208512Z","iopub.execute_input":"2024-03-16T17:46:20.208806Z","iopub.status.idle":"2024-03-16T17:46:20.229129Z","shell.execute_reply.started":"2024-03-16T17:46:20.208782Z","shell.execute_reply":"2024-03-16T17:46:20.228286Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"code","source":"traindf_640.query(f\"image_id == '{image_id}'\")","metadata":{"execution":{"iopub.status.busy":"2024-03-16T17:46:20.230304Z","iopub.execute_input":"2024-03-16T17:46:20.230675Z","iopub.status.idle":"2024-03-16T17:46:20.251748Z","shell.execute_reply.started":"2024-03-16T17:46:20.230642Z","shell.execute_reply":"2024-03-16T17:46:20.25096Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"code","source":"display_image_with_boxes(image_id, TRAIN_640, traindf_640) # Original 640x640 image annotations\ndisplay_image_with_boxes(image_id, TRAIN_640, wbf_640) # Image annotation with WBF","metadata":{"execution":{"iopub.status.busy":"2024-03-16T17:46:20.252598Z","iopub.execute_input":"2024-03-16T17:46:20.252825Z","iopub.status.idle":"2024-03-16T17:46:20.887118Z","shell.execute_reply.started":"2024-03-16T17:46:20.252804Z","shell.execute_reply":"2024-03-16T17:46:20.88618Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"markdown","source":"Both the output and images show that the Weighted Box Fusion has been successful, but let us analyze more images","metadata":{}},{"cell_type":"code","source":"display_image_with_boxes(\"001d127bad87592efe45a5c7678f8b8d\", TRAIN_640, traindf_640) # Original 640x640 image annotations\ndisplay_image_with_boxes(\"001d127bad87592efe45a5c7678f8b8d\", TRAIN_640, wbf_640) # Image annotation with WBF\ndisplay_image_with_boxes(\"051132a778e61a86eb147c7c6f564dfe\", TRAIN_640, traindf_640) # Original 640x640 image annotations\ndisplay_image_with_boxes(\"051132a778e61a86eb147c7c6f564dfe\", TRAIN_640, wbf_640) # Image annotation with WBF\ndisplay_image_with_boxes(\"1c32170b4af4ce1a3030eb8167753b06\", TRAIN_640, traindf_640) # Original 640x640 image annotations\ndisplay_image_with_boxes(\"1c32170b4af4ce1a3030eb8167753b06\", TRAIN_640, wbf_640) # Image annotation with WBF","metadata":{"execution":{"iopub.status.busy":"2024-03-16T17:46:20.894413Z","iopub.execute_input":"2024-03-16T17:46:20.894793Z","iopub.status.idle":"2024-03-16T17:46:22.564372Z","shell.execute_reply.started":"2024-03-16T17:46:20.894767Z","shell.execute_reply":"2024-03-16T17:46:22.56348Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"code","source":"wbf_640.query(f\"image_id == '1c32170b4af4ce1a3030eb8167753b06'\")","metadata":{"execution":{"iopub.status.busy":"2024-03-16T17:46:22.565552Z","iopub.execute_input":"2024-03-16T17:46:22.565884Z","iopub.status.idle":"2024-03-16T17:46:22.588871Z","shell.execute_reply.started":"2024-03-16T17:46:22.565853Z","shell.execute_reply":"2024-03-16T17:46:22.587777Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"markdown","source":"# **ML Model Learning (YOLOv8)**\n\nNow that we have preprocessed our data, we can train our model.  \nFirst, let us create 3 different dataframes :\n* One dataset where we leave the boxes judged an abnormality by 1 or more radiologists.\n* One dataset where we leave the boxes judged an abnormality by 2 or more radiologists.\n* One dataset where we leave the boxes judged an abnormality by all 3 radiologist.","metadata":{}},{"cell_type":"code","source":"# First, create a dataframe where we add the class 14 images to the wbf_640 dataframe.\n# We know all 3 radiologists always agree for class 14. \n# Therefore we add a single row for each image_id where class_id = 14 and append it count = 3\n\n# Filter traindf_640 to get rows where class_id = 14 (\"No finding\")\n#no_finding_df = traindf_640[traindf_640['class_id'] == 14].drop_duplicates(subset=['image_id'])\n\n# Since all annotations for \"No finding\" are consistent across radiologists,\n# we can directly assign counts = 3.0 for these and prepare the dataframe accordingly\n#no_finding_df['count'] = 3.0\n#no_finding_df['score'] = 1.0\n\n# Assuming 'traindf_640' and 'wbf_640' have the same structure and column names, \n# but 'wbf_640' lacks rows for \"No finding\". We'll add a simplified row for each 'no_finding_df' entry back to 'wbf_640'.\n# If 'wbf_640' doesn't have a 'contributing_boxes_count' column, make sure to add it or adjust as necessary.\n# Reset index for both DataFrames to ensure they have default integer indexes\n#wbf_640 = wbf_640.reset_index(drop=True)\n#no_finding_df = no_finding_df.reset_index(drop=True)\n\n# Ensure both DataFrames have exactly the same columns\n# Assuming wbf_640 already contains 'contributing_boxes_count', if not, adjust as necessary\n#required_columns = wbf_640.columns.tolist()  # Get the list of columns from wbf_640\n#no_finding_df = no_finding_df[required_columns]  # Select only the matching columns from no_finding_df\n\n# Now that both DataFrames have matching columns and reset indexes, concatenate them\n#final_df = pd.concat([wbf_640, no_finding_df], ignore_index=True)","metadata":{"execution":{"iopub.status.busy":"2024-03-16T17:46:22.5901Z","iopub.execute_input":"2024-03-16T17:46:22.590436Z","iopub.status.idle":"2024-03-16T17:46:22.598494Z","shell.execute_reply.started":"2024-03-16T17:46:22.590386Z","shell.execute_reply":"2024-03-16T17:46:22.5977Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"markdown","source":"Reading further upon yolo training, images that do not have a matching .txt file can still be used during training, effectively as a negative sample that contains no instances of the classes you're training the model to detect. Therefore it is not essential to put back class 14, as we can just train the model to find class 14 : no finding by not matching it a .txt file in the occurence of a class_id = 14 annotation.","metadata":{}},{"cell_type":"code","source":"wbf_640['class_id'] = wbf_640['class_id'].astype(int)\nwbf_640.query(f\"image_id == '1c32170b4af4ce1a3030eb8167753b06'\")","metadata":{"execution":{"iopub.status.busy":"2024-03-16T17:46:22.599566Z","iopub.execute_input":"2024-03-16T17:46:22.599824Z","iopub.status.idle":"2024-03-16T17:46:22.628108Z","shell.execute_reply.started":"2024-03-16T17:46:22.599798Z","shell.execute_reply":"2024-03-16T17:46:22.62727Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"code","source":"wbf_640.tail()","metadata":{"execution":{"iopub.status.busy":"2024-03-16T17:46:22.629125Z","iopub.execute_input":"2024-03-16T17:46:22.629389Z","iopub.status.idle":"2024-03-16T17:46:22.642891Z","shell.execute_reply.started":"2024-03-16T17:46:22.629356Z","shell.execute_reply":"2024-03-16T17:46:22.641947Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"code","source":"print(len(train_df)) # All annotations for all images. 15k images\nprint(len(traindf_640))\nprint(len(traindf_640_filtered)) # All annotations for all abnormalities\nprint(len(wbf_640)) # All annotations for all abnormalities after wbf\n# print(len(final_df)) # All annotations after wbf and fusing the class 14 rows for each image\n\n# print(((len(traindf_640) - len(traindf_640_filtered))/3) - (len(final_df)-len(wbf_640))) # Should be = 0","metadata":{"execution":{"iopub.status.busy":"2024-03-16T17:46:22.644186Z","iopub.execute_input":"2024-03-16T17:46:22.644538Z","iopub.status.idle":"2024-03-16T17:46:22.650958Z","shell.execute_reply.started":"2024-03-16T17:46:22.644506Z","shell.execute_reply":"2024-03-16T17:46:22.649934Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"markdown","source":" --------------------------------------------------------------------------------------------------------","metadata":{}},{"cell_type":"markdown","source":"**Code to create yolo_data dataset. does not need to be used again **","metadata":{}},{"cell_type":"code","source":"# df_count_ge_1 = wbf_640[wbf_640['count'] >= 1]\n# df_count_ge_2 = wbf_640[wbf_640['count'] >= 2]\n# df_count_eq_3 = wbf_640[wbf_640['count'] == 3]\n# \n# # Creating the directories for the labels : /kaggle/working/yolov8_datasets/dataset_ge_1/labels/train\n# # Img directory for training is still : TRAIN_640 = '/kaggle/input/train-dir-640x640/TRAIN_DIR_JPG_640x640'\n# base_dir = \"/kaggle/working/yolov8_datasets\"\n# datasets = [\"dataset_ge_1\", \"dataset_ge_2\", \"dataset_eq_3\"]\n# for ds in datasets:\n#     os.makedirs(os.path.join(base_dir, ds, \"labels/train\"), exist_ok=True)\n# ","metadata":{"execution":{"iopub.status.busy":"2024-03-16T17:46:22.65212Z","iopub.execute_input":"2024-03-16T17:46:22.65241Z","iopub.status.idle":"2024-03-16T17:46:22.662749Z","shell.execute_reply.started":"2024-03-16T17:46:22.652385Z","shell.execute_reply":"2024-03-16T17:46:22.662033Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"code","source":"# def create_annotations(filtered_df, dataset_path):\n#     for _, row in filtered_df.iterrows():\n#         img_id = row['image_id']\n#         class_id = int(row['class_id'])  # Make sure class_id is an integer\n#         # Normalize the bbox coordinates\n#         x_center = ((row['x_min'] + row['x_max']) / 2) / 640\n#         y_center = ((row['y_min'] + row['y_max']) / 2) / 640\n#         width = (row['x_max'] - row['x_min']) / 640\n#         height = (row['y_max'] - row['y_min']) / 640\n#         # Annotation line format for YOLO\n#         annotation_line = f\"{class_id} {x_center} {y_center} {width} {height}\\n\"\n#         \n#         # Write to file\n#         with open(os.path.join(dataset_path, \"labels/train\", f\"{img_id}.txt\"), \"a\") as file:\n#             file.write(annotation_line)","metadata":{"execution":{"iopub.status.busy":"2024-03-16T17:46:22.663785Z","iopub.execute_input":"2024-03-16T17:46:22.664041Z","iopub.status.idle":"2024-03-16T17:46:22.672675Z","shell.execute_reply.started":"2024-03-16T17:46:22.664018Z","shell.execute_reply":"2024-03-16T17:46:22.671742Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"code","source":"# create_annotations(df_count_ge_1, os.path.join(base_dir, \"dataset_ge_1\"))\n# create_annotations(df_count_ge_2, os.path.join(base_dir, \"dataset_ge_2\"))\n# create_annotations(df_count_eq_3, os.path.join(base_dir, \"dataset_eq_3\"))","metadata":{"execution":{"iopub.status.busy":"2024-03-16T17:46:22.673539Z","iopub.execute_input":"2024-03-16T17:46:22.673795Z","iopub.status.idle":"2024-03-16T17:46:22.681524Z","shell.execute_reply.started":"2024-03-16T17:46:22.673772Z","shell.execute_reply":"2024-03-16T17:46:22.680768Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"markdown","source":"--------------------------------------------------------------------------------------------------------- ","metadata":{}},{"cell_type":"code","source":"# Retrieve and set the wandb API key\n#from kaggle_secrets import UserSecretsClient\n#user_secrets = UserSecretsClient()\n#wandb_api_key = user_secrets.get_secret(\"WANDB_API_KEY\")\n#\n#import os\n#os.environ[\"WANDB_API_KEY\"] = wandb_api_key","metadata":{"execution":{"iopub.status.busy":"2024-03-16T17:46:22.682791Z","iopub.execute_input":"2024-03-16T17:46:22.683051Z","iopub.status.idle":"2024-03-16T17:46:22.695719Z","shell.execute_reply.started":"2024-03-16T17:46:22.683028Z","shell.execute_reply":"2024-03-16T17:46:22.6949Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"code","source":"!pip install -U ipywidgets\n!pip install ultralytics\nfrom ultralytics import YOLO","metadata":{"execution":{"iopub.status.busy":"2024-03-16T17:46:22.696673Z","iopub.execute_input":"2024-03-16T17:46:22.696919Z","iopub.status.idle":"2024-03-16T17:46:47.475271Z","shell.execute_reply.started":"2024-03-16T17:46:22.696887Z","shell.execute_reply":"2024-03-16T17:46:47.474055Z"},"_kg_hide-output":true,"collapsed":true,"jupyter":{"outputs_hidden":true},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"markdown","source":"\n# **IMPORTANT**  \n\nThe code below has been used to get the results_ge_1 dataset, which holds the results of the training, and the necessary files to do our testing.   \n\nBecause of time, space, and computation power constraints, even though it was planned to train multiple models based on confidence given the number of radiologists having annoted a same abnormality, **only one model has been trained with YOLOV8n**, which is the nano version of YOLOV8 : It is fast but not as precise as the other versions of YOLOV8. Therefore, the result of that submission will not reach the expectations I had set at first. However, as a first project, it is still a good learning experience.","metadata":{}},{"cell_type":"code","source":"#from kaggle_secrets import UserSecretsClient\n#user_secrets = UserSecretsClient()\n#secret_value_0 = user_secrets.get_secret(\"WANDB_API_KEY\")\n\n#model = YOLO(\"yolov8n.yaml\")\n#results = model.train(data=\"/kaggle/input/yolo-data/dataset_ge_1/config_ge_1.yml\", epochs = 50)","metadata":{"execution":{"iopub.status.busy":"2024-03-16T17:46:47.476734Z","iopub.execute_input":"2024-03-16T17:46:47.477048Z","iopub.status.idle":"2024-03-16T17:46:47.482458Z","shell.execute_reply.started":"2024-03-16T17:46:47.477015Z","shell.execute_reply":"2024-03-16T17:46:47.481453Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"markdown","source":"# **Testing**","metadata":{}},{"cell_type":"code","source":"# Load the trained model\nmodel_path = '/kaggle/input/results-ge-1/runs/detect/train/weights/best.pt' \nmodel = YOLO(model_path)\n\n# Perform inference on the test set\ntest_jpg_dir = '/kaggle/input/yolo-data/dataset_ge_1/images/test'\ntest_jpg_paths = [os.path.join(test_jpg_dir, f_name) for f_name in os.listdir(test_jpg_dir)]\nresults = model.predict(source=test_jpg_dir)","metadata":{"execution":{"iopub.status.busy":"2024-03-16T17:46:47.483605Z","iopub.execute_input":"2024-03-16T17:46:47.483866Z","iopub.status.idle":"2024-03-16T17:47:34.116022Z","shell.execute_reply.started":"2024-03-16T17:46:47.483844Z","shell.execute_reply":"2024-03-16T17:47:34.11532Z"},"_kg_hide-output":true,"collapsed":true,"jupyter":{"outputs_hidden":true},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"markdown","source":"We have obtained the test results. Now, we want to convert those results into the correct csv submission file format to be able to evaluate those results.","metadata":{}},{"cell_type":"code","source":"# This code helps us understand the format of the results\n\nspecific_image_id = '7073d58de75ee80e16ec3a4458349f90'  \n# Iterate over image paths and corresponding predictions\nfor img_path, pred in zip(test_jpg_paths, results):\n    # Extract image_id from img_path\n    image_id = os.path.basename(img_path).split('.')[0]\n    \n    # Check if the current image_id matches the image ID\n    if image_id == specific_image_id:\n        # Inspect the prediction\n        if hasattr(pred, 'boxes'):\n            boxes = pred.boxes\n            print(f\"Prediction for image {image_id}: {boxes}\")","metadata":{"execution":{"iopub.status.busy":"2024-03-16T17:47:34.1177Z","iopub.execute_input":"2024-03-16T17:47:34.117949Z","iopub.status.idle":"2024-03-16T17:47:34.137966Z","shell.execute_reply.started":"2024-03-16T17:47:34.117925Z","shell.execute_reply":"2024-03-16T17:47:34.137157Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"code","source":"test_original_dims_csv = (\"/kaggle/input/images-original-sizes/test_original_dimensions.csv\")  # Adjust path as needed\ntest_original_dims_df = pd.read_csv(test_original_dims_csv)\n\n# Convert to dictionary mapping image_id to a tuple of (width, height)\noriginal_dimensions_test = {row['image_id']: (row['width'], row['height']) for index, row in test_original_dims_df.iterrows()}","metadata":{"execution":{"iopub.status.busy":"2024-03-16T17:47:34.138969Z","iopub.execute_input":"2024-03-16T17:47:34.139231Z","iopub.status.idle":"2024-03-16T17:47:34.339103Z","shell.execute_reply.started":"2024-03-16T17:47:34.139208Z","shell.execute_reply":"2024-03-16T17:47:34.338267Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"code","source":"# Function that adjusts the bbox from 640x640 to original dimensions, taking into account the padding to save aspect ratio.\n# This is because the submission file must have the boundary boxes for the original size images\n\ndef adjust_detection_bbox(xmin, ymin, xmax, ymax, image_id, dimensions_dict):\n    # Retrieve the original dimensions of the image\n    original_width, original_height = dimensions_dict[image_id]\n    \n    # Calculate the ratio used for resizing while maintaining aspect ratio\n    ratio = min(640 / original_width, 640 / original_height)\n    \n    # Calculate padding offsets based on the new size after resizing\n    new_width = int(original_width * ratio)\n    new_height = int(original_height * ratio)\n    pad_x = (640 - new_width) // 2\n    pad_y = (640 - new_height) // 2\n    \n    # Adjust coordinates to account for padding added to maintain aspect ratio\n    xmin = max(0, xmin - pad_x) / ratio\n    xmax = max(0, xmax - pad_x) / ratio\n    ymin = max(0, ymin - pad_y) / ratio\n    ymax = max(0, ymax - pad_y) / ratio\n    \n    return xmin, ymin, xmax, ymax","metadata":{"execution":{"iopub.status.busy":"2024-03-16T17:47:34.340311Z","iopub.execute_input":"2024-03-16T17:47:34.34092Z","iopub.status.idle":"2024-03-16T17:47:34.348143Z","shell.execute_reply.started":"2024-03-16T17:47:34.340891Z","shell.execute_reply":"2024-03-16T17:47:34.347158Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"markdown","source":"**Results conversion into submission format**  ","metadata":{}},{"cell_type":"code","source":"# Code to transform the test results into the correct submission file format.\n\n# Sort test_jpg_paths in alphabetical order to match the results alignment\ntest_jpg_paths = sorted(test_jpg_paths)\n\npredictions = []\n\nfor img_path, pred in zip(test_jpg_paths, results):\n    image_id = os.path.basename(img_path).split('.')[0]\n    prediction_string = ''\n    finding_score = 0  # Initialize finding_score to capture the highest confidence score among detections\n\n    if hasattr(pred, 'boxes') and pred.boxes:\n        # Directly access bounding box coordinates, confidence scores, and class IDs\n        boxes_xyxy = pred.boxes.xyxy.cpu().numpy()  # Bounding box coordinates\n        confs = pred.boxes.conf.cpu().numpy()  # Confidence scores\n        cls_ids = pred.boxes.cls.cpu().numpy()  # Class IDs\n        \n        for i, box in enumerate(boxes_xyxy):\n            xmin, ymin, xmax, ymax = box\n            conf = confs[i]  # Access corresponding confidence score\n            cls = cls_ids[i]  # Access corresponding class ID\n            \n            # Adjust bounding box coordinates\n            xmin_adj, ymin_adj, xmax_adj, ymax_adj = adjust_detection_bbox(xmin, ymin, xmax, ymax, image_id, original_dimensions_test)\n            \n            # Update prediction string\n            prediction_string += f'{int(cls)} {conf} {xmin_adj} {ymin_adj} {xmax_adj} {ymax_adj} '\n            finding_score = max(finding_score, conf)\n\n    # Condition for appending \"No finding\"\n    if finding_score == 0:# or conf < (1 - finding_score):\n        prediction_string += '14 1 0 0 1 1'\n\n    predictions.append((image_id, prediction_string.strip()))\n\n# Convert predictions into a DataFrame for submission\nsubmission_df = pd.DataFrame(predictions, columns=['image_id', 'PredictionString'])\n\n","metadata":{"execution":{"iopub.status.busy":"2024-03-16T17:47:34.349262Z","iopub.execute_input":"2024-03-16T17:47:34.3496Z","iopub.status.idle":"2024-03-16T17:47:34.560638Z","shell.execute_reply.started":"2024-03-16T17:47:34.349575Z","shell.execute_reply":"2024-03-16T17:47:34.559663Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"code","source":"submission_df.to_csv('/kaggle/working/submission_ge_1.csv', index=False)\nsubmission_df.query(f\"image_id == '008bdde2af2462e86fd373a445d0f4cd'\")","metadata":{"execution":{"iopub.status.busy":"2024-03-16T17:47:34.561827Z","iopub.execute_input":"2024-03-16T17:47:34.562144Z","iopub.status.idle":"2024-03-16T17:47:34.590725Z","shell.execute_reply.started":"2024-03-16T17:47:34.562119Z","shell.execute_reply":"2024-03-16T17:47:34.589693Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"code","source":"submission_df.head(20)","metadata":{"execution":{"iopub.status.busy":"2024-03-16T17:47:34.59177Z","iopub.execute_input":"2024-03-16T17:47:34.592052Z","iopub.status.idle":"2024-03-16T17:47:34.604522Z","shell.execute_reply.started":"2024-03-16T17:47:34.592027Z","shell.execute_reply":"2024-03-16T17:47:34.603632Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"markdown","source":"# **Analysis**\n\nNow that we have trained and tested our model, we transform submission_df into a dataframe that allows analysis, for example from our image function we have used earlier.  \nWe also analyze graphs we obtained from the training of our model","metadata":{}},{"cell_type":"code","source":"# Function to get a dataframe usable by previous functions from submission_df\n\ndef parse_prediction_string(row, class_id_to_name):\n    items = row['PredictionString'].split(' ')\n    boxes = []\n    for i in range(0, len(items), 6):\n        class_id, score, xmin, ymin, xmax, ymax = items[i:i+6]\n        boxes.append({\n            'image_id': row['image_id'],\n            'class_id': int(float(class_id)),  # Ensure class_id is treated as integer\n            'score': float(score),\n            'x_min': float(xmin),\n            'y_min': float(ymin),\n            'x_max': float(xmax),\n            'y_max': float(ymax),\n            'class_name': class_id_to_name.get(float(class_id), \"Unknown\")  # Map class_id to class_name\n        })\n    return boxes","metadata":{"execution":{"iopub.status.busy":"2024-03-16T17:47:34.605651Z","iopub.execute_input":"2024-03-16T17:47:34.605958Z","iopub.status.idle":"2024-03-16T17:47:34.613846Z","shell.execute_reply.started":"2024-03-16T17:47:34.605934Z","shell.execute_reply":"2024-03-16T17:47:34.613064Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"code","source":"boxes_data = []\nfor _, row in submission_df.iterrows():\n    boxes_data.extend(parse_prediction_string(row, class_id_to_name))\n\nanalysis_df = pd.DataFrame(boxes_data)\nanalysis_df.head(10)","metadata":{"execution":{"iopub.status.busy":"2024-03-16T18:38:12.857063Z","iopub.execute_input":"2024-03-16T18:38:12.857499Z","iopub.status.idle":"2024-03-16T18:38:13.092289Z","shell.execute_reply.started":"2024-03-16T18:38:12.857462Z","shell.execute_reply":"2024-03-16T18:38:13.091286Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"code","source":"analysis_df.query(f\"image_id == '074e3cceb0c1677a1ad0dbff31167d7d'\")","metadata":{"execution":{"iopub.status.busy":"2024-03-16T17:47:34.842278Z","iopub.execute_input":"2024-03-16T17:47:34.843682Z","iopub.status.idle":"2024-03-16T17:47:34.859636Z","shell.execute_reply.started":"2024-03-16T17:47:34.843655Z","shell.execute_reply":"2024-03-16T17:47:34.858659Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"code","source":"# Adjusting bounding box coordinates for each row in the DataFrame\n\nTEST_640 = \"/kaggle/input/yolo-data/dataset_ge_1/images/test\"\n\n# We convert the boundary boxes back for them to fit the converted 640x640 test images so we can analyze them\nanalysis_df = analysis_df.apply(lambda row: adjust_bbox(row, original_dimensions_test), axis=1)\ndisplay_image_with_boxes(\"074e3cceb0c1677a1ad0dbff31167d7d\", TEST_640, analysis_df)","metadata":{"execution":{"iopub.status.busy":"2024-03-16T17:47:34.860846Z","iopub.execute_input":"2024-03-16T17:47:34.861258Z","iopub.status.idle":"2024-03-16T17:47:35.679165Z","shell.execute_reply.started":"2024-03-16T17:47:34.861226Z","shell.execute_reply":"2024-03-16T17:47:35.678263Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"markdown","source":"The image has been attributed multiple abnormalities. The boundary boxes seem correctly placed.","metadata":{}},{"cell_type":"code","source":"from IPython.display import Image\nImage('/kaggle/input/results-ge-1/runs/detect/train/confusion_matrix_normalized.png')","metadata":{"execution":{"iopub.status.busy":"2024-03-16T19:46:35.255702Z","iopub.execute_input":"2024-03-16T19:46:35.256071Z","iopub.status.idle":"2024-03-16T19:46:35.271Z","shell.execute_reply.started":"2024-03-16T19:46:35.256042Z","shell.execute_reply":"2024-03-16T19:46:35.269862Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"markdown","source":"This confusion matrix shows that background class, or the no finding class, is heavily weighted. It shows that there is an imbalance in our model that we are well aware of and that we expected : The number of no finding cases is vastly superior to the number of other classes.  \nThis reinforces the need of trying multiple models.  \nTraining on images that have abnormalities only is a prospect that is interesting, as well as ensembling models that train on all images and on only abnormality images.  \nThis was my original plan but I will sadly not have enough time to do so.  \n\nAortic enlargement and Cardiomegaly have the best prediction rates as seen on the diagonal (True positives)  \nHowever, all of the other classes (abnormalities) have significantly lower true positivs. Their prediction rates are much lower.","metadata":{}},{"cell_type":"code","source":"Image('/kaggle/input/results-ge-1/runs/detect/train/results.png')","metadata":{"execution":{"iopub.status.busy":"2024-03-16T19:45:40.50593Z","iopub.execute_input":"2024-03-16T19:45:40.506395Z","iopub.status.idle":"2024-03-16T19:45:40.5217Z","shell.execute_reply.started":"2024-03-16T19:45:40.50636Z","shell.execute_reply":"2024-03-16T19:45:40.520759Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"markdown","source":"First, looking at train and val loss curves, we see both the training and validation loss curves decreasing over time without a significant gap between them. This suggests good generalization without overfitting, as well as learning and model improvement.\n\nSecond, looking at the metric/mAP50 curve, this metric represents the mean Average Precision at 50% IoU (Intersection over Union) threshold. It is an overall performance metric for object detection models, taking into account both precision and recall across different thresholds.  \nIn the competition we are evaluated on the testing on mAP at 40%IoU. \nIn this validation graph, the mAP is increasing, showing the model's performance is improving over time, and reaches over 0.3.\n","metadata":{}},{"cell_type":"markdown","source":"References:  \nhttps://www.kaggle.com/code/sreevishnudamodaran/vinbigdata-fusing-bboxes-coco-dataset  \nhttps://www.kaggle.com/competitions/vinbigdata-chest-xray-abnormalities-detection/discussion/229786  \nhttps://www.kaggle.com/code/corochann/vinbigdata-2-class-classifier-complete-pipeline   \nhttps://www.kaggle.com/code/nxhong93/yolov5-chest-512  \nhttps://arxiv.org/abs/1910.13302  \nhttps://github.com/ZFTurbo/Weighted-Boxes-Fusion  \nhttps://www.kaggle.com/code/gabrielmaire/image-transformation-2/  \nhttps://www.kaggle.com/code/gabrielmaire/original-sizes-csv","metadata":{}}]}