{"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":"# Are we training on the original dataset (and thus need to resize it, etc)?\nRUN_ON_ORIGINAL = True\n\n# Run only inference tasks? (You still need the separate noteboook for now.)\nINFERENCE_ONLY = True\n\nYOLOV5_REPO = '/kaggle/input/ultralyticsyolov5a'\nKAGGLE_DATASET = '/kaggle/input/siim-covid19-detection'\n\nPROJECT_NAME = 'kaggle-siim-covid'\nEXP_NAME = 'exp'\n\nWEIGHTS_FILE = 'best.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/'\nTEST_PATH = KAGGLE_RESIZED + '/test/'\n\n# Path to the final inference model:\nMODEL_PATH = f'{PROJECT_NAME}/{EXP_NAME}/weights/{WEIGHTS_FILE}'","metadata":{"execution":{"iopub.status.busy":"2021-06-18T08:16:44.10162Z","iopub.execute_input":"2021-06-18T08:16:44.10195Z","iopub.status.idle":"2021-06-18T08:16:44.108387Z","shell.execute_reply.started":"2021-06-18T08:16:44.101921Z","shell.execute_reply":"2021-06-18T08:16:44.107283Z"},"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 {YOLOV5_REPO} yolov5\n\n# YOLOv5 default weights:\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!mkdir -p yolov5/{PROJECT_NAME}/{EXP_NAME}/weights/\n!cp /kaggle/input/siim-covid19-yolov5-weights/{WEIGHTS_FILE} yolov5/{PROJECT_NAME}/{EXP_NAME}/weights/","metadata":{"execution":{"iopub.status.busy":"2021-06-18T08:01:24.953777Z","iopub.execute_input":"2021-06-18T08:01:24.954124Z","iopub.status.idle":"2021-06-18T08:01:55.822074Z","shell.execute_reply.started":"2021-06-18T08:01:24.954069Z","shell.execute_reply":"2021-06-18T08:01:55.820964Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"code","source":"import os, pathlib\n\n# This is an inference-only notebook, don't.\nWANT_WANDB = False\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-18T08:01:55.825696Z","iopub.execute_input":"2021-06-18T08:01:55.825958Z","iopub.status.idle":"2021-06-18T08:01:58.112048Z","shell.execute_reply.started":"2021-06-18T08:01:55.825928Z","shell.execute_reply":"2021-06-18T08:01:58.111206Z"},"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-18T08:01:58.115186Z","iopub.execute_input":"2021-06-18T08:01:58.115454Z","iopub.status.idle":"2021-06-18T08:01:58.122225Z","shell.execute_reply.started":"2021-06-18T08:01:58.115426Z","shell.execute_reply":"2021-06-18T08:01:58.121392Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"markdown","source":"# 🔨 Prepare Dataset\n\nThis is the most important section when it comes to training an object detector with YOLOv5. The directory structure, bounding box format, etc must be in the correct order. This section builds every piece needed to train a YOLOv5 model.\n\nI am using [xhlulu's](https://www.kaggle.com/xhlulu) resized dataset. The uploaded 256x256 Kaggle dataset is [here](https://www.kaggle.com/xhlulu/siim-covid19-resized-to-256px-jpg). Find other image resolutions [here](https://www.kaggle.com/c/siim-covid19-detection/discussion/239918).\n\n* Create train-validation split. <br>\n* Create required `/dataset` folder structure and more the images to that folder. <br>\n* Create `data.yaml` file needed to train the model. <br>\n* Create bounding box coordinates in the required YOLO format. ","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-18T08:01:58.12397Z","iopub.execute_input":"2021-06-18T08:01:58.124566Z","iopub.status.idle":"2021-06-18T08:09:49.509839Z","shell.execute_reply.started":"2021-06-18T08:01:58.124522Z","shell.execute_reply":"2021-06-18T08:09:49.508932Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"code","source":"%cd /kaggle/working/yolov5\n!python detect.py --weights {MODEL_PATH} \\\n                  --source {TEST_PATH} \\\n                  --img {IMG_SIZE} \\\n                  --project {PROJECT_NAME} \\\n                  --name {EXP_NAME} \\\n                  --conf 0.281 \\\n                  --iou-thres 0.5 \\\n                  --max-det 3 \\\n                  --save-txt \\\n                  --save-conf","metadata":{"scrolled":true,"_kg_hide-output":true,"execution":{"iopub.status.busy":"2021-06-18T08:17:19.125288Z","iopub.execute_input":"2021-06-18T08:17:19.125661Z","iopub.status.idle":"2021-06-18T08:18:28.38352Z","shell.execute_reply.started":"2021-06-18T08:17:19.125626Z","shell.execute_reply":"2021-06-18T08:18:28.382559Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"code","source":"PRED_PATH = f'/kaggle/working/yolov5/{PROJECT_NAME}/{EXP_NAME}2/labels'\nprediction_files = os.listdir(PRED_PATH)\n!ls {PRED_PATH}\n\nprint('Number of test images predicted as opaque: ', len(prediction_files))","metadata":{"scrolled":true,"_kg_hide-output":true,"execution":{"iopub.status.busy":"2021-06-18T08:27:48.749365Z","iopub.execute_input":"2021-06-18T08:27:48.749713Z","iopub.status.idle":"2021-06-18T08:27:49.39933Z","shell.execute_reply.started":"2021-06-18T08:27:48.749682Z","shell.execute_reply":"2021-06-18T08:27:49.398079Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"markdown","source":"# Submission","metadata":{}},{"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')\n#meta_df.head(5)\n\ntest_meta_df = meta_df.loc[meta_df.split == 'test']\ntest_meta_df = test_meta_df.drop('split', axis=1)\ntest_meta_df.columns = ['id', 'dim0', 'dim1']\n\n#test_meta_df.head(5)","metadata":{"execution":{"iopub.status.busy":"2021-06-18T08:27:54.449935Z","iopub.execute_input":"2021-06-18T08:27:54.450259Z","iopub.status.idle":"2021-06-18T08:27:54.473527Z","shell.execute_reply.started":"2021-06-18T08:27:54.450227Z","shell.execute_reply":"2021-06-18T08:27:54.472713Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"code","source":"# The submisison requires xmin, ymin, xmax, ymax format. \n# YOLOv5 returns x_center, y_center, width, height\n# We also need to rescale the box to the original size!\ndef correct_bbox_format(bboxes, img_name):\n    original_size = test_meta_df.loc[test_meta_df.id == img_name]\n    orig_width = original_size['dim0']\n    orig_height = original_size['dim1']\n    \n    correct_bboxes = []\n    for b in bboxes:\n        xc, yc = int(np.round(b[0]*orig_width)), int(np.round(b[1]*orig_height))\n        w, h = int(np.round(b[2]*orig_width)), int(np.round(b[3]*orig_height))\n\n        xmin = xc - int(np.round(w/2))\n        xmax = xc + int(np.round(w/2))\n        ymin = yc - int(np.round(h/2))\n        ymax = yc + int(np.round(h/2))\n        \n        correct_bboxes.append([xmin, xmax, ymin, ymax])\n        \n    return correct_bboxes\n\n# Read the txt file generated by YOLOv5 during inference and extract \n# confidence and bounding box coordinates.\ndef get_conf_bboxes(file_path):\n    confidence = []\n    bboxes = []\n    with open(file_path, 'r') as file:\n        for line in file:\n            preds = line.strip('\\n').split(' ')\n            preds = list(map(float, preds))\n            confidence.append(preds[-1])\n            bboxes.append(preds[1:-1])\n    return confidence, bboxes","metadata":{"_kg_hide-input":false,"execution":{"iopub.status.busy":"2021-06-18T08:28:02.739972Z","iopub.execute_input":"2021-06-18T08:28:02.740317Z","iopub.status.idle":"2021-06-18T08:28:02.750033Z","shell.execute_reply.started":"2021-06-18T08:28:02.740289Z","shell.execute_reply":"2021-06-18T08:28:02.749184Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"code","source":"# Read the submisison file\nsub_df = pd.read_csv(KAGGLE_DATASET + '/sample_submission.csv')\nsub_df.tail()","metadata":{"execution":{"iopub.status.busy":"2021-06-18T08:28:07.753485Z","iopub.execute_input":"2021-06-18T08:28:07.75382Z","iopub.status.idle":"2021-06-18T08:28:07.776158Z","shell.execute_reply.started":"2021-06-18T08:28:07.753789Z","shell.execute_reply":"2021-06-18T08:28:07.774953Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"code","source":"# Prediction loop for submission\npredictions = []\n\nfor i in tqdm(range(len(sub_df))):\n    row = sub_df.loc[i]\n    id_name = row.id.split('_')[0]\n    id_level = row.id.split('_')[-1]\n    \n    if id_level == 'study':\n        # do study-level classification\n        predictions.append(\"negative 1 0 0 1 1\") # dummy prediction\n        \n    elif id_level == 'image':\n        # we can do image-level classification here.\n        # also we can rely on the object detector's classification head.\n        # for this example submisison we will use YOLO's classification head. \n        # since we already ran the inference we know which test images belong to opacity.\n        if f'{id_name}.txt' in prediction_files:\n            # opacity label\n            confidence, bboxes = get_conf_bboxes(f'{PRED_PATH}/{id_name}.txt')\n            bboxes = correct_bbox_format(bboxes, id_name)\n            pred_string = ''\n            for j, conf in enumerate(confidence):\n                pred_string += f'opacity {conf} ' + ' '.join(map(str, bboxes[j])) + ' '\n            predictions.append(pred_string[:-1])\n        else:\n            predictions.append(\"none 1 0 0 1 1\")","metadata":{"execution":{"iopub.status.busy":"2021-06-18T08:28:20.634156Z","iopub.execute_input":"2021-06-18T08:28:20.634532Z","iopub.status.idle":"2021-06-18T08:28:21.267871Z","shell.execute_reply.started":"2021-06-18T08:28:20.63449Z","shell.execute_reply":"2021-06-18T08:28:21.266946Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"code","source":"sub_df['PredictionString'] = predictions\nsub_df.to_csv('/kaggle/working/submission.csv', index=False)\nsub_df.tail()","metadata":{"execution":{"iopub.status.busy":"2021-06-18T08:28:23.477101Z","iopub.execute_input":"2021-06-18T08:28:23.47746Z","iopub.status.idle":"2021-06-18T08:28:23.508499Z","shell.execute_reply.started":"2021-06-18T08:28:23.477427Z","shell.execute_reply":"2021-06-18T08:28:23.507635Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"code","source":"# Force nuke all remaining files, as the submission interface gets confused with too many of those:\n%cd /kaggle/working\n!rm -rf /kaggle/working/yolov5\n!rm -rf /kaggle/working/gdcm\n!rm -rf /kaggle/working/gdcm.tar","metadata":{"execution":{"iopub.status.busy":"2021-06-18T08:28:26.16672Z","iopub.execute_input":"2021-06-18T08:28:26.167128Z","iopub.status.idle":"2021-06-18T08:28:27.539936Z","shell.execute_reply.started":"2021-06-18T08:28:26.167091Z","shell.execute_reply":"2021-06-18T08:28:27.538892Z"},"trusted":true},"execution_count":null,"outputs":[]}]}