{"metadata":{"kernelspec":{"language":"python","display_name":"Python 3","name":"python3"},"language_info":{"pygments_lexer":"ipython3","nbconvert_exporter":"python","version":"3.6.4","file_extension":".py","codemirror_mode":{"name":"ipython","version":3},"name":"python","mimetype":"text/x-python"}},"nbformat_minor":4,"nbformat":4,"cells":[{"cell_type":"markdown","source":"This kernel is based primarily on:\n\n- Competition: https://www.kaggle.com/c/siim-covid19-detection/overview\n- Starter kernel: https://www.kaggle.com/ayuraj/train-covid-19-detection-using-yolov5\n- Primary dataset: https://www.kaggle.com/c/siim-covid19-detection/data\n- Resized dataset: https://www.kaggle.com/xhlulu/siim-covid19-resized-to-1024px-jpg (others [here](https://www.kaggle.com/c/siim-covid19-detection/discussion/239918))\n- Detection model: YOLOv5 - https://github.com/ultralytics/yolov5\n- Tracking: Weights and Biases (integrated with YOLOv5)\n\nNotes:\n\n- RUN THE EXPERIMENT ON A GPU INSTANCE! This is going to take a sweet, sweet while either way.","metadata":{}},{"cell_type":"markdown","source":"# Setup and Variables","metadata":{}},{"cell_type":"code","source":"# WANT_WANDB defined in the initial setup block.\n# Are we training on the original dataset (and thus need to resize it, etc)?\nRUN_ON_ORIGINAL = False\n\n# Run only inference tasks? (You still need the separate noteboook for now.)\nINFERENCE_ONLY = False\n\nYOLOV5_REPO = '/kaggle/input/ultralyticsyolov5a'\nKAGGLE_DATASET = '/kaggle/input/siim-covid19-detection'\n\nPROJECT_NAME = 'kaggle-siim-covid'\nWEIGHTS_FILE = 'yolov5s.pt'\nIMG_SIZE = 1024\nBATCH_SIZE = 16\nEPOCHS = 10\n\n# Pick the data source to train on:\nKAGGLE_RESIZED = '/kaggle/tmp' if RUN_ON_ORIGINAL else f'/kaggle/input/siim-covid19-resized-to-{IMG_SIZE}px-jpg'\nTRAIN_PATH = KAGGLE_RESIZED + '/train/'","metadata":{"execution":{"iopub.status.busy":"2021-06-18T10:17:57.537734Z","iopub.execute_input":"2021-06-18T10:17:57.538138Z","iopub.status.idle":"2021-06-18T10:17:57.549070Z","shell.execute_reply.started":"2021-06-18T10:17:57.538056Z","shell.execute_reply":"2021-06-18T10:17:57.547808Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"code","source":"%cd /kaggle/working\n\n# GDCM:\n!cp /kaggle/input/gdcm-conda-install/gdcm.tar .\n!tar -xvzf gdcm.tar\n!conda install --offline ./gdcm/gdcm-2.8.9-py37h71b2a6d_0.tar.bz2\n\n# YOLOv5:\n!cp -r /kaggle/input/ultralyticsyolov5a yolov5\n!cp /kaggle/input/ultralyticsyolov5aweights/* yolov5/\n\n# If you want to supply your own pretrained model, dump it into a\n# dataset and copy it over to the YOLOv5 directory here.\n#!cp /kaggle/input/some-stuff/best.pt yolov5/\n\n# Are we training on the original dataset (and thus need to resize it, etc)?\nTRAIN_ON_ORIGINAL = False","metadata":{"execution":{"iopub.status.busy":"2021-06-18T10:18:48.168360Z","iopub.execute_input":"2021-06-18T10:18:48.168761Z","iopub.status.idle":"2021-06-18T10:19:10.364191Z","shell.execute_reply.started":"2021-06-18T10:18:48.168727Z","shell.execute_reply":"2021-06-18T10:19:10.363118Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"code","source":"import os, pathlib\n\n# If we're training on original data, we don't want to save it into W&B for now.\nWANT_WANDB = not RUN_ON_ORIGINAL\nif WANT_WANDB:\n    from kaggle_secrets import UserSecretsClient\n    user_secrets = UserSecretsClient()\n    os.environ['WANDB_API_KEY'] = UserSecretsClient().get_secret(\"WANDB_KEY\")\n\n    import wandb\n    wandb.login()\n\nimport gc\nimport cv2\nimport numpy as np\nimport pandas as pd\nfrom PIL import Image\nfrom tqdm.auto import tqdm\nfrom shutil import copyfile\nimport matplotlib.pyplot as plt\nfrom sklearn.model_selection import train_test_split\n\nimport pydicom\nfrom pydicom.pixel_data_handlers.util import apply_voi_lut\n\nimport torch\nprint(f\"Using torch {torch.__version__} ({torch.cuda.get_device_properties(0).name if torch.cuda.is_available() else 'CPU'})\")","metadata":{"execution":{"iopub.status.busy":"2021-06-18T10:20:33.729938Z","iopub.execute_input":"2021-06-18T10:20:33.730370Z","iopub.status.idle":"2021-06-18T10:20:37.368749Z","shell.execute_reply.started":"2021-06-18T10:20:33.730338Z","shell.execute_reply":"2021-06-18T10:20:37.367810Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"code","source":"#customize iPython writefile so we can write variables\nfrom IPython.core.magic import register_line_cell_magic\n\n@register_line_cell_magic\ndef writetemplate(line, cell):\n    with open(line, 'w') as f:\n        f.write(cell.format(**globals()))","metadata":{"_uuid":"8f2839f25d086af736a60e9eeb907d3b93b6e0e5","_cell_guid":"b1076dfc-b9ad-4769-8c92-a6c4dae69d19","execution":{"iopub.status.busy":"2021-06-18T10:20:40.735341Z","iopub.execute_input":"2021-06-18T10:20:40.735660Z","iopub.status.idle":"2021-06-18T10:20:40.740945Z","shell.execute_reply.started":"2021-06-18T10:20:40.735631Z","shell.execute_reply":"2021-06-18T10:20:40.739891Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"markdown","source":"# Convert data\n\nOriginal data is in the DICOM format which is hard to work with. Convert it to something we can use first (if needed).","metadata":{}},{"cell_type":"code","source":"def read_xray(path, voi_lut = True, fix_monochrome = True):\n    # Original from: https://www.kaggle.com/raddar/convert-dicom-to-np-array-the-correct-way\n    dicom = pydicom.read_file(path)\n    \n    # VOI LUT (if available by DICOM device) is used to transform raw DICOM data to \n    # \"human-friendly\" view\n    if voi_lut:\n        data = apply_voi_lut(dicom.pixel_array, dicom)\n    else:\n        data = dicom.pixel_array\n               \n    # depending on this value, X-ray may look inverted - fix that:\n    if fix_monochrome and dicom.PhotometricInterpretation == \"MONOCHROME1\":\n        data = np.amax(data) - data\n        \n    data = data - np.min(data)\n    data = data / np.max(data)\n    data = (data * 255).astype(np.uint8)\n        \n    return data\n\ndef resize(array, size, keep_ratio=False, resample=Image.LANCZOS):\n    # Original from: https://www.kaggle.com/xhlulu/vinbigdata-process-and-resize-to-image\n    im = Image.fromarray(array)\n    \n    if keep_ratio:\n        im.thumbnail((size, size), resample)\n    else:\n        im = im.resize((size, size), resample)\n    \n    return im\n\ndef convert_dataset():\n    image_id = []\n    dim0 = []\n    dim1 = []\n    splits = []\n    \n    if INFERENCE_ONLY:\n        valid_splits = ['test']\n    else:\n        valid_splits = ['test', 'train']\n\n    # NOTE: For inference only, all you need is test:\n    for split in valid_splits:\n        save_dir = f'{KAGGLE_RESIZED}/{split}/'\n\n        os.makedirs(save_dir, exist_ok=True)\n\n        for dirname, _, filenames in tqdm(os.walk(f'{KAGGLE_DATASET}/{split}')):\n            for file in filenames:\n                # set keep_ratio=True to have original aspect ratio\n                xray = read_xray(os.path.join(dirname, file))\n                im = resize(xray, size=IMG_SIZE)\n                im.save(os.path.join(save_dir, file.replace('dcm', 'jpg')))\n\n                image_id.append(file.replace('.dcm', ''))\n                dim0.append(xray.shape[0])\n                dim1.append(xray.shape[1])\n                splits.append(split)\n\n    df = pd.DataFrame.from_dict({'image_id': image_id, 'dim0': dim0, 'dim1': dim1, 'split': splits})\n    df.to_csv(KAGGLE_RESIZED + '/meta.csv', index=False)\n\n\nif RUN_ON_ORIGINAL:\n    convert_dataset()","metadata":{"execution":{"iopub.status.busy":"2021-06-18T10:20:43.119031Z","iopub.execute_input":"2021-06-18T10:20:43.119408Z","iopub.status.idle":"2021-06-18T10:20:43.132559Z","shell.execute_reply.started":"2021-06-18T10:20:43.119379Z","shell.execute_reply":"2021-06-18T10:20:43.131335Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"markdown","source":"# Prepare Dataset\n\nYOLOv5 requires its dataset in a COCO dataset format.","metadata":{}},{"cell_type":"code","source":"# Everything is done from /kaggle directory.\n%cd /kaggle\n\n#df_labels_long = [\"Negative for Pneumonia\", \"Typical Appearance\", \"Indeterminate Appearance\", \"Atypical Appearance\"]\ndf_labels_long = [\"_2\", \"_3\", \"_4\", \"_5\"]\ndf_labels = [\"none\", \"negative\", \"typical\", \"indterminate\", \"atypical\"]\nimage_label_map = {}\n\ndef get_label_from_row(row):\n    for i in range(len(df_labels_long)):\n        if getattr(row, df_labels_long[i]) > 0:\n            return df_labels[i + 1]\n    return df_labels[0]\n\nstudy_df = pd.read_csv(KAGGLE_DATASET + '/train_study_level.csv')\nfor row in study_df.itertuples():\n    image_label_map[row.id.split('_')[0]] = get_label_from_row(row)\n\n# Load image level csv file\ndf = pd.read_csv(KAGGLE_DATASET + '/train_image_level.csv')\n\n# Modify values in the id column\ndf['id'] = df.apply(lambda row: row.id.split('_')[0], axis=1)\n# Add absolute path\ndf['path'] = df.apply(lambda row: TRAIN_PATH+row.id+'.jpg', axis=1)\n# Get image level labels\ndf['image_level'] = df.apply(lambda row: image_label_map[row.StudyInstanceUID], axis=1)\n\ndf.head(5)","metadata":{"execution":{"iopub.status.busy":"2021-06-18T10:23:09.828119Z","iopub.execute_input":"2021-06-18T10:23:09.828502Z","iopub.status.idle":"2021-06-18T10:23:10.160033Z","shell.execute_reply.started":"2021-06-18T10:23:09.828470Z","shell.execute_reply":"2021-06-18T10:23:10.159007Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"code","source":"# Load meta.csv file\n# Original dimensions are required to scale the bounding box coordinates appropriately.\nmeta_df = pd.read_csv(KAGGLE_RESIZED + '/meta.csv')\ntrain_meta_df = meta_df.loc[meta_df.split == 'train']\ntrain_meta_df = train_meta_df.drop('split', axis=1)\ntrain_meta_df.columns = ['id', 'dim0', 'dim1']\n\ntrain_meta_df.head(2)","metadata":{"execution":{"iopub.status.busy":"2021-06-18T10:23:16.830365Z","iopub.execute_input":"2021-06-18T10:23:16.830693Z","iopub.status.idle":"2021-06-18T10:23:16.865686Z","shell.execute_reply.started":"2021-06-18T10:23:16.830665Z","shell.execute_reply":"2021-06-18T10:23:16.864706Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"code","source":"# Merge both the dataframes\ndf = df.merge(train_meta_df, on='id',how=\"left\")\ndf.head(2)","metadata":{"execution":{"iopub.status.busy":"2021-06-18T10:23:18.940783Z","iopub.execute_input":"2021-06-18T10:23:18.941161Z","iopub.status.idle":"2021-06-18T10:23:18.969351Z","shell.execute_reply.started":"2021-06-18T10:23:18.941125Z","shell.execute_reply":"2021-06-18T10:23:18.968379Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"code","source":"# Create train and validation split.\ntrain_df, valid_df = train_test_split(df, test_size=0.2, random_state=42, stratify=df.image_level.values)\n\ntrain_df.loc[:, 'split'] = 'train'\nvalid_df.loc[:, 'split'] = 'valid'\n\ndf = pd.concat([train_df, valid_df]).reset_index(drop=True)","metadata":{"_kg_hide-input":true,"_kg_hide-output":true,"execution":{"iopub.status.busy":"2021-06-18T10:23:20.691430Z","iopub.execute_input":"2021-06-18T10:23:20.691765Z","iopub.status.idle":"2021-06-18T10:23:20.751410Z","shell.execute_reply.started":"2021-06-18T10:23:20.691734Z","shell.execute_reply":"2021-06-18T10:23:20.749517Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"code","source":"print(f'Size of dataset: {len(df)}, training images: {len(train_df)}. validation images: {len(valid_df)}')","metadata":{"execution":{"iopub.status.busy":"2021-06-18T10:23:23.020526Z","iopub.execute_input":"2021-06-18T10:23:23.020905Z","iopub.status.idle":"2021-06-18T10:23:23.028484Z","shell.execute_reply.started":"2021-06-18T10:23:23.020863Z","shell.execute_reply":"2021-06-18T10:23:23.027471Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"markdown","source":"## 🍚 Prepare Required Folder Structure\n\nThe required folder structure for the dataset directory is: \n\n```\n/parent_folder\n    /dataset\n         /images\n             /train\n             /val\n         /labels\n             /train\n             /val\n    /yolov5\n```\n\nNote that I have named the directory `covid`.","metadata":{}},{"cell_type":"code","source":"os.makedirs('/kaggle/working/covid/images/train', exist_ok=True)\nos.makedirs('/kaggle/working/covid/images/valid', exist_ok=True)\n\nos.makedirs('/kaggle/working/covid/labels/train', exist_ok=True)\nos.makedirs('/kaggle/working/covid/labels/valid', exist_ok=True)\n\n! ls /kaggle/working/covid/images","metadata":{"execution":{"iopub.status.busy":"2021-06-18T10:23:25.503591Z","iopub.execute_input":"2021-06-18T10:23:25.503922Z","iopub.status.idle":"2021-06-18T10:23:26.181607Z","shell.execute_reply.started":"2021-06-18T10:23:25.503892Z","shell.execute_reply":"2021-06-18T10:23:26.180461Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"code","source":"# Move the images to relevant split folder.\nfor i in tqdm(range(len(df))):\n    row = df.loc[i]\n    if row.split == 'train':\n        copyfile(row.path, f'/kaggle/working/covid/images/train/{row.id}.jpg')\n    else:\n        copyfile(row.path, f'/kaggle/working/covid/images/valid/{row.id}.jpg')","metadata":{"execution":{"iopub.status.busy":"2021-06-18T10:23:26.855638Z","iopub.execute_input":"2021-06-18T10:23:26.856005Z","iopub.status.idle":"2021-06-18T10:24:13.870150Z","shell.execute_reply.started":"2021-06-18T10:23:26.855968Z","shell.execute_reply":"2021-06-18T10:24:13.869104Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"markdown","source":"## 🍜 Create `.YAML` file\n\nThe `data.yaml`, is the dataset configuration file that defines \n\n1. an \"optional\" download command/URL for auto-downloading, \n2. a path to a directory of training images (or path to a *.txt file with a list of training images), \n3. a path to a directory of validation images (or path to a *.txt file with a list of validation images), \n4. the number of classes, \n5. a list of class names.\n\n> 📍 Important: In this competition, each image can either belong to `opacity` or `none` image-level labels. That's why I have  used the number of classes, `nc` to be 2. YOLOv5 automatically handles the images without any bounding box coordinates. \n\n> 📍 Note: The `data.yaml` is created in the `yolov5/data` directory as required. ","metadata":{}},{"cell_type":"code","source":"# Create .yaml file \nimport yaml\n\ndata_yaml = dict(\n    train = '/kaggle/working/covid/images/train',\n    val = '/kaggle/working/covid/images/valid',\n    nc = 5,\n    names = df_labels\n)\n\n# Note that I am creating the file in the yolov5/data/ directory.\nwith open('/kaggle/working/yolov5/data/data.yaml', 'w') as outfile:\n    yaml.dump(data_yaml, outfile, default_flow_style=True)\n    \n%cat /kaggle/working/yolov5/data/data.yaml","metadata":{"execution":{"iopub.status.busy":"2021-06-18T10:24:17.671592Z","iopub.execute_input":"2021-06-18T10:24:17.671928Z","iopub.status.idle":"2021-06-18T10:24:18.355376Z","shell.execute_reply.started":"2021-06-18T10:24:17.671898Z","shell.execute_reply":"2021-06-18T10:24:18.354233Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"markdown","source":"## 🍮 Prepare Bounding Box Coordinated for YOLOv5\n\nFor every image with **bounding box(es)** a `.txt` file with the same name as the image will be created in the format shown below:\n\n* One row per object. <br>\n* Each row is class `x_center y_center width height format`. <br>\n* Box coordinates must be in normalized xywh format (from 0 - 1). We can normalize by the boxes in pixels by dividing `x_center` and `width` by image width, and `y_center` and `height` by image height. <br>\n* Class numbers are zero-indexed (start from 0). <br>\n\n> 📍 Note: We don't have to remove the images without bounding boxes from the training or validation sets. ","metadata":{}},{"cell_type":"code","source":"# Get the raw bounding box by parsing the row value of the label column.\n# Ref: https://www.kaggle.com/yujiariyasu/plot-3positive-classes\ndef get_bbox(row):\n    bboxes = []\n    bbox = []\n    for i, l in enumerate(row.label.split(' ')):\n        if (i % 6 == 0) | (i % 6 == 1):\n            continue\n        bbox.append(float(l))\n        if i % 6 == 5:\n            bboxes.append(bbox)\n            bbox = []  \n            \n    return bboxes\n\n# Scale the bounding boxes according to the size of the resized image. \ndef scale_bbox(row, bboxes):\n    # Get scaling factor\n    scale_x = IMG_SIZE/row.dim1\n    scale_y = IMG_SIZE/row.dim0\n    \n    scaled_bboxes = []\n    for bbox in bboxes:\n        x = int(np.round(bbox[0]*scale_x, 4))\n        y = int(np.round(bbox[1]*scale_y, 4))\n        x1 = int(np.round(bbox[2]*(scale_x), 4))\n        y1= int(np.round(bbox[3]*scale_y, 4))\n\n        scaled_bboxes.append([x, y, x1, y1]) # xmin, ymin, xmax, ymax\n        \n    return scaled_bboxes\n\n# Convert the bounding boxes in YOLO format.\ndef get_yolo_format_bbox(img_w, img_h, bboxes):\n    yolo_boxes = []\n    for bbox in bboxes:\n        w = bbox[2] - bbox[0] # xmax - xmin\n        h = bbox[3] - bbox[1] # ymax - ymin\n        xc = bbox[0] + int(np.round(w/2)) # xmin + width/2\n        yc = bbox[1] + int(np.round(h/2)) # ymin + height/2\n        \n        yolo_boxes.append([xc/img_w, yc/img_h, w/img_w, h/img_h]) # x_center y_center width height\n    \n    return yolo_boxes","metadata":{"execution":{"iopub.status.busy":"2021-06-18T10:24:22.739757Z","iopub.execute_input":"2021-06-18T10:24:22.740119Z","iopub.status.idle":"2021-06-18T10:24:22.750773Z","shell.execute_reply.started":"2021-06-18T10:24:22.740085Z","shell.execute_reply":"2021-06-18T10:24:22.749586Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"code","source":"# Prepare the txt files for bounding box\nfor i in tqdm(range(len(df))):\n    row = df.loc[i]\n    # Get image id\n    img_id = row.id\n    # Get split\n    split = row.split\n    # Get image-level label\n    label = row.image_level\n    \n    if row.split=='train':\n        file_name = f'/kaggle/working/covid/labels/train/{row.id}.txt'\n    else:\n        file_name = f'/kaggle/working/covid/labels/valid/{row.id}.txt'\n        \n    \n    if label!='none':\n        # Get bboxes\n        bboxes = get_bbox(row)\n        # Scale bounding boxes\n        scale_bboxes = scale_bbox(row, bboxes)\n        # Format for YOLOv5\n        yolo_bboxes = get_yolo_format_bbox(IMG_SIZE, IMG_SIZE, scale_bboxes)\n        \n        with open(file_name, 'w') as f:\n            for bbox in yolo_bboxes:\n                bbox = [df_labels.index(label)]+bbox\n                bbox = [str(i) for i in bbox]\n                bbox = ' '.join(bbox)\n                f.write(bbox)\n                f.write('\\n')","metadata":{"execution":{"iopub.status.busy":"2021-06-18T10:24:26.225882Z","iopub.execute_input":"2021-06-18T10:24:26.226286Z","iopub.status.idle":"2021-06-18T10:24:29.812063Z","shell.execute_reply.started":"2021-06-18T10:24:26.226252Z","shell.execute_reply":"2021-06-18T10:24:29.811096Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"markdown","source":"# Train","metadata":{}},{"cell_type":"code","source":"%cd /kaggle/working/yolov5/\n!python train.py --img {IMG_SIZE} \\\n                 --batch {BATCH_SIZE} \\\n                 --epochs {EPOCHS} \\\n                 --data data.yaml \\\n                 --weights {WEIGHTS_FILE} \\\n                 --save_period 1 \\\n                 --project {PROJECT_NAME}","metadata":{"execution":{"iopub.status.busy":"2021-06-18T10:24:33.503147Z","iopub.execute_input":"2021-06-18T10:24:33.503469Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"markdown","source":"The best model is automatically uploaded to W&B where it can be downloaded and uploaded into a Kaggle dataset (for use in a separate inference notebook).","metadata":{}}]}