{"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":"none","dataSources":[{"sourceId":10338,"databundleVersionId":862042,"sourceType":"competition"},{"sourceId":7663783,"sourceType":"datasetVersion","datasetId":4469083}],"dockerImageVersionId":30646,"isInternetEnabled":true,"language":"python","sourceType":"notebook","isGpuEnabled":false}},"nbformat_minor":4,"nbformat":4,"cells":[{"cell_type":"markdown","source":"# YOLOv8 object detection\n\nIn this notebook we will implement simple object detection using YOLOv8 and create submission\n\n#### Install ultralytics","metadata":{"_uuid":"fca6a60a-8191-453c-88e2-dd3756e89caa","_cell_guid":"bcafa96c-affc-4f4a-a831-f917f3b230d6","trusted":true}},{"cell_type":"code","source":"%pip install ultralytics\nimport ultralytics\nultralytics.checks()","metadata":{"_uuid":"434aff39-4936-45fc-8288-4bff3bb129a3","_cell_guid":"aaac273f-637b-4845-b923-68d8957c3b62","collapsed":false,"jupyter":{"outputs_hidden":false},"execution":{"iopub.status.busy":"2024-02-20T13:29:05.755745Z","iopub.execute_input":"2024-02-20T13:29:05.757179Z","iopub.status.idle":"2024-02-20T13:29:36.720531Z","shell.execute_reply.started":"2024-02-20T13:29:05.757130Z","shell.execute_reply":"2024-02-20T13:29:36.719364Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"markdown","source":"#### Add all needed packages","metadata":{"_uuid":"6133b8e1-a2a2-4512-92e2-1b73744ef6f2","_cell_guid":"31ad97b5-df1d-43bd-a440-a8be56e2c203","trusted":true}},{"cell_type":"code","source":"import pydicom\nimport cv2\nfrom tqdm import tqdm\nimport pandas as pd\nimport os\nimport matplotlib.pyplot as plt\nimport yaml\nimport os\nimport shutil\nimport numpy as np\nfrom sklearn.model_selection import train_test_split\n\n\n# set random seed for reproducibility\nrandom_state = 42\nnp.random.seed(random_state)","metadata":{"_uuid":"59701099-16a0-4212-b697-415752af6142","_cell_guid":"cc0ba701-f786-4b49-abd0-28c3af1c0309","collapsed":false,"jupyter":{"outputs_hidden":false},"execution":{"iopub.status.busy":"2024-02-20T13:29:38.348615Z","iopub.execute_input":"2024-02-20T13:29:38.349355Z","iopub.status.idle":"2024-02-20T13:29:39.167160Z","shell.execute_reply.started":"2024-02-20T13:29:38.349312Z","shell.execute_reply":"2024-02-20T13:29:39.165730Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"markdown","source":"## Dataset Preprocessing\n\nDataset for YOLOv8 training has to have the specific structure. There are several options, but we will use the following one:\n\n- config.yam\n- train\n     - images\n     - labels\n- val\n    - images\n    - labels\n\n[more information about datasets structure](https://docs.ultralytics.com/hub/datasets/)\n\n#### 1. Create folders in current working directory (/kaggle/working)","metadata":{"_uuid":"14abe4b1-d252-480e-9d49-7074ac85e4f6","_cell_guid":"c63ef97c-3c6e-421b-8a0e-0058f9803e2b","trusted":true}},{"cell_type":"code","source":"img_dir = os.path.join(os.getcwd(), \"images\")\nlabel_dir = os.path.join(os.getcwd(), \"labels\")\ntrain_images = os.path.join(img_dir, \"train\")\nval_images = os.path.join(img_dir, \"val\")\ntrain_labels = os.path.join(label_dir, \"train\")\nval_labels = os.path.join(label_dir, \"val\")\ntest_images=os.path.join(os.getcwd(),'test_images')\n\nnew_folders = [train_images,val_images,train_labels,val_labels,test_images]\nfor folder in new_folders:\n    os.makedirs(folder,exist_ok=True)","metadata":{"_uuid":"844fe968-38b3-41ec-a44a-ca665710e3c7","_cell_guid":"7b6ec680-776f-446d-9bdf-508b927019da","collapsed":false,"jupyter":{"outputs_hidden":false},"execution":{"iopub.status.busy":"2024-02-20T13:54:04.285785Z","iopub.execute_input":"2024-02-20T13:54:04.286288Z","iopub.status.idle":"2024-02-20T13:54:04.294393Z","shell.execute_reply.started":"2024-02-20T13:54:04.286253Z","shell.execute_reply":"2024-02-20T13:54:04.293563Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"markdown","source":"#### 2. Load labels and explore the data","metadata":{}},{"cell_type":"code","source":"rsna_dataset_input_path = '/kaggle/input/rsna-pneumonia-detection-challenge'\ntrain_labels_df = pd.read_csv(os.path.join(rsna_dataset_input_path,'stage_2_train_labels.csv'))\ntrain_labels_df.head()","metadata":{"execution":{"iopub.status.busy":"2024-02-20T13:29:43.954015Z","iopub.execute_input":"2024-02-20T13:29:43.954440Z","iopub.status.idle":"2024-02-20T13:29:44.066567Z","shell.execute_reply.started":"2024-02-20T13:29:43.954408Z","shell.execute_reply":"2024-02-20T13:29:44.065045Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"markdown","source":"#### 3. Split data into training/validation sets","metadata":{}},{"cell_type":"code","source":"patient_ids = train_labels_df.patientId.drop_duplicates()\nprint(f'Total number of observations: {len(patient_ids)}')\n\ntrain_ids, val_ids = train_test_split(patient_ids, test_size = 0.2, random_state=random_state)\nprint(f'Train observations: {len(train_ids)}\\nTest observations: {len(val_ids)}')","metadata":{"execution":{"iopub.status.busy":"2024-02-20T13:29:46.142849Z","iopub.execute_input":"2024-02-20T13:29:46.144440Z","iopub.status.idle":"2024-02-20T13:29:46.181743Z","shell.execute_reply.started":"2024-02-20T13:29:46.144362Z","shell.execute_reply":"2024-02-20T13:29:46.180482Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"markdown","source":"#### 4. Convert RSNA-pneumonia detection dataset to YOLOv8 format\n\nThis operation requires a few steps:\n- Convert dicom images to jpg and place in the correct folder (images/train, images/val)\n- Convert bounding boxes from __x, y, width, height__ to relative __center_x, center_y, width, height__ to follow YOLO format\n- Write for each observation class and bounding box to the .txt file in the corresponding folder (labels/train, labels/val). If there are no bounding boxes on the observation, we should not create a file. YOLO will mark them as backgound for training. The txt labels should look the following:\n__{class} {center_x_rel} {center_y_rel} {width_rel} {height_rel}__\n\n\n\n##### 4.1 Convert dicom images to jpg\n","metadata":{}},{"cell_type":"code","source":"# dcm image size\nimg_size = 1024\n\ndef dcm_to_jpg(dcm_dir,img_dir,patient_id):\n    jpg_img_path = os.path.join(img_dir, f'{patient_id}.jpg')\n    if os.path.exists(jpg_img_path):\n        return\n    dcm_img_path = os.path.join(dcm_dir, f'{patient_id}.dcm')\n    if not os.path.exists(dcm_img_path):\n        return\n    img_greyscale = pydicom.read_file(dcm_img_path).pixel_array\n    img_rgb = np.stack([img_greyscale]*3, -1)\n    cv2.imwrite(jpg_img_path,img_rgb)\n    \ndef label_row_to_txt_file(label_dir,patient_id,data=None):\n    if pd.isnull(data).any():\n        return\n    \n    # convert to relative coordinates\n    x_rel = data[0]/img_size\n    y_rel = data[1]/img_size\n    width_rel = data[2]/img_size\n    height_rel = data[3]/img_size\n    \n    center_x_rel = x_rel + width_rel/2\n    center_y_rel = y_rel + height_rel/2\n    \n    label_path = os.path.join(label_dir,f'{patient_id}.txt')\n    \n    with open(label_path, 'a+') as file:\n        # The first 0 in string is static becuase we have only one class\n        line = f'0 {center_x_rel} {center_y_rel} {width_rel} {height_rel}\\n'\n        file.write(line)\n        \ndef data_to_yolo_format(inut_img_dir, train_labels_df, img_dir, label_dir, train_ids):\n    for row in tqdm(train_labels_df.values):\n        patient_id = row[0]\n        data = row[1:]\n        \n        # based on the train_ids select the correct root folder for images and labels\n        final_dir_name = 'train' if patient_id in train_ids else 'val'\n        img_final_dir =  os.path.join(img_dir,  final_dir_name)\n        label_final_dir = os.path.join(label_dir,  final_dir_name)\n        \n        dcm_to_jpg(inut_img_dir,img_final_dir,patient_id)\n        label_row_to_txt_file(label_final_dir,patient_id,data)\n\n# used for test data to convert all dcm images from folder to jpg at once\ndef dcm_to_jpg_folder(dcm_dir,img_dir):\n    for filename in tqdm(os.listdir(dcm_dir)):\n        patient_id = os.path.splitext(filename)[0]\n        dcm_to_jpg(dcm_dir,img_dir,patient_id)","metadata":{"execution":{"iopub.status.busy":"2024-02-20T13:52:53.615482Z","iopub.execute_input":"2024-02-20T13:52:53.615939Z","iopub.status.idle":"2024-02-20T13:52:53.633725Z","shell.execute_reply.started":"2024-02-20T13:52:53.615904Z","shell.execute_reply":"2024-02-20T13:52:53.632132Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"markdown","source":"#### Convert data","metadata":{}},{"cell_type":"code","source":"train_images_input = os.path.join(rsna_dataset_input_path,'stage_2_train_images')\ntest_images_input = os.path.join(rsna_dataset_input_path,'stage_2_test_images')\ndata_to_yolo_format(train_images_input,train_labels_df, img_dir, label_dir,train_ids.values )\ndcm_to_jpg_folder(test_images_input, test_images)","metadata":{"execution":{"iopub.status.busy":"2024-02-20T13:29:49.734578Z","iopub.execute_input":"2024-02-20T13:29:49.735080Z","iopub.status.idle":"2024-02-20T13:52:20.025492Z","shell.execute_reply.started":"2024-02-20T13:29:49.735038Z","shell.execute_reply":"2024-02-20T13:52:20.023658Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"markdown","source":"At this point you can save the notebook and create private dataset from the output of the notebook to avoid wasting time when you restart the session.\n\nIf you decided to do so, comment the cell above and here is the helper method of how to obtain the train/val patient ids from the imported private dataset:","metadata":{}},{"cell_type":"code","source":"def get_patient_ids(folder_path):\n    patient_ids = []\n    for filename in os.listdir(folder_path):\n        patient_id = os.path.splitext(filename)[0]\n        patient_ids.append(patient_id)\n    return patient_ids","metadata":{"execution":{"iopub.status.busy":"2024-02-20T13:56:03.774402Z","iopub.execute_input":"2024-02-20T13:56:03.774930Z","iopub.status.idle":"2024-02-20T13:56:03.783400Z","shell.execute_reply.started":"2024-02-20T13:56:03.774890Z","shell.execute_reply":"2024-02-20T13:56:03.782064Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"markdown","source":"Update the path here, uncomment and enjoy!","metadata":{}},{"cell_type":"code","source":"# private_dataset_input = '/kaggle/input/{private_dataset_name}/images'\n\n# train_ids = get_patient_ids(os.path.join(private_dataset_input,'train'))\n# val_ids = get_patient_ids(os.path.join(private_dataset_input,'val')","metadata":{"execution":{"iopub.status.busy":"2024-02-20T13:56:05.154122Z","iopub.execute_input":"2024-02-20T13:56:05.154680Z","iopub.status.idle":"2024-02-20T13:56:05.161203Z","shell.execute_reply.started":"2024-02-20T13:56:05.154637Z","shell.execute_reply":"2024-02-20T13:56:05.159511Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"markdown","source":"#### Create config.yaml file\n\nIf you are using at this point private dataset, change __path__ to __/kaggle/input/{private_dataset_name}__","metadata":{}},{"cell_type":"code","source":"data = {\n    'path': f'{os.getcwd()}',\n    'train': 'images/train',\n    'val': 'images/val',\n    'cache_dir': '/kaggle/working',\n    'names': {\n        0: 'pneumonia'\n    }\n}\n\n# Save YAML to file\npath_to_yaml = os.path.join(os.getcwd(),'config.yaml')\nwith open(path_to_yaml, 'w') as file:\n    yaml.dump(data, file, default_flow_style=False, sort_keys=False, indent=4,  allow_unicode=False,)","metadata":{"execution":{"iopub.status.busy":"2024-02-20T13:56:06.465941Z","iopub.execute_input":"2024-02-20T13:56:06.466420Z","iopub.status.idle":"2024-02-20T13:56:06.475226Z","shell.execute_reply.started":"2024-02-20T13:56:06.466387Z","shell.execute_reply":"2024-02-20T13:56:06.473946Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"markdown","source":"Now preprocessing is done!\n\n\n#### Visualize the train image\nAgain, if you are using private dataset, update paths to images/labels","metadata":{}},{"cell_type":"code","source":"type_1_ids = train_labels_df[train_labels_df.Target == 1].patientId\ntest_patient_id = type_1_ids[type_1_ids.isin(train_ids)].iloc[100]\n\nimg_test_id = os.path.join(train_images, f\"{test_patient_id}.jpg\")\nlatbel_test_id = os.path.join(train_labels, f\"{test_patient_id}.txt\")\n\nplt.imshow(cv2.imread(img_test_id))\n\nwith open(latbel_test_id, \"r\") as file:\n    for line in file:\n        class_id, rcx, rcy, rw, rh = list(map(float, line.strip().split()))\n        x = (rcx-rw/2)*img_size\n        y = (rcy-rh/2)*img_size\n        w = rw*img_size\n        h = rh*img_size\n        plt.plot([x, x, x+w, x+w, x], [y, y+h, y+h, y, y])","metadata":{"execution":{"iopub.status.busy":"2024-02-20T13:56:09.104735Z","iopub.execute_input":"2024-02-20T13:56:09.105191Z","iopub.status.idle":"2024-02-20T13:56:09.709017Z","shell.execute_reply.started":"2024-02-20T13:56:09.105156Z","shell.execute_reply":"2024-02-20T13:56:09.708034Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"markdown","source":"#### Train yolo model\n\nThe train is commented to avoid long output\n\nP.S. Tips for newbies like me: do not train models in the session, save notebook version and it will run in the cloud, providing the needed output.","metadata":{}},{"cell_type":"code","source":"from ultralytics import YOLO\n\nmodel = YOLO('yolov8n.pt')\n# results = model.train(data='config.yaml', epochs=100, imgsz=640, batch=-1)\n# results.confusion_matrix.matrix","metadata":{"execution":{"iopub.status.busy":"2024-02-20T13:56:12.760040Z","iopub.execute_input":"2024-02-20T13:56:12.760475Z","iopub.status.idle":"2024-02-20T13:56:13.707742Z","shell.execute_reply.started":"2024-02-20T13:56:12.760441Z","shell.execute_reply":"2024-02-20T13:56:13.706546Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"markdown","source":"#### Select the best model. \nIt is located in __kaggle/working/runs/detect/train/weights/best.pt__ or you can specify the specific path using __cache_dir__ property in __config.yaml__ file.\n\nIn this notebook I will use preimported model weights, trained using the same code as above.","metadata":{}},{"cell_type":"code","source":"best_weights_path = '/kaggle/input/best-100-epochs/best.pt'\nmodel_best = YOLO(best_weights_path)","metadata":{"execution":{"iopub.status.busy":"2024-02-20T13:56:24.808892Z","iopub.execute_input":"2024-02-20T13:56:24.809419Z","iopub.status.idle":"2024-02-20T13:56:24.982954Z","shell.execute_reply.started":"2024-02-20T13:56:24.809384Z","shell.execute_reply":"2024-02-20T13:56:24.982061Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"markdown","source":"#### Test prediction\n\nWe will use predict on the same image, visualized before.","metadata":{}},{"cell_type":"code","source":"results = model_best.predict(img_test_id,save=True)\nplt.imshow(cv2.imread(f'/kaggle/working/runs/detect/predict/{test_patient_id}.jpg'))","metadata":{"execution":{"iopub.status.busy":"2024-02-20T13:58:07.503709Z","iopub.execute_input":"2024-02-20T13:58:07.504255Z","iopub.status.idle":"2024-02-20T13:58:08.277576Z","shell.execute_reply.started":"2024-02-20T13:58:07.504221Z","shell.execute_reply":"2024-02-20T13:58:08.276465Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"markdown","source":"### Predict on all test images\n\nExploring the model results, I have found out that the model has quite low confidence, but the good detection results overall. Therefore we need to lower the confidence for prediction. In general, it should be tuned on the traininig set, but in this particular case it was selected empirically.","metadata":{}},{"cell_type":"code","source":"results = model_best.predict(test_images,conf=0.08,verbose=False,stream=True)","metadata":{"execution":{"iopub.status.busy":"2024-02-20T14:06:09.437748Z","iopub.execute_input":"2024-02-20T14:06:09.438214Z","iopub.status.idle":"2024-02-20T14:06:09.445456Z","shell.execute_reply.started":"2024-02-20T14:06:09.438181Z","shell.execute_reply":"2024-02-20T14:06:09.444021Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"markdown","source":"### Create submission","metadata":{}},{"cell_type":"code","source":"submit_dict = {\"patientId\": [], \"PredictionString\": []}\n\nfor result in tqdm(results, total=len(os.listdir(test_images))):\n    patient_id = os.path.splitext(os.path.basename(result.path))[0]\n    submit_dict[\"patientId\"].append(patient_id)\n    submit_line=''\n    for b in result.boxes:\n        xywhn = b.xywhn.tolist()\n        rcx, rcy, rw, rh = xywhn[0][0],xywhn[0][1],xywhn[0][2],xywhn[0][3]\n        conf = float(b.conf)\n        # convert bbox back to original format from relative\n        x = (rcx-rw/2)*img_size\n        y = (rcy-rh/2)*img_size\n        w = rw*img_size\n        h = rh*img_size\n        submit_line += f\"{conf} {x} {y} {w} {h} \"\n    submit_dict[\"PredictionString\"].append(submit_line)\npd.DataFrame(submit_dict).to_csv('submission.csv', index=False)","metadata":{"execution":{"iopub.status.busy":"2024-02-20T14:06:37.429588Z","iopub.execute_input":"2024-02-20T14:06:37.430811Z","iopub.status.idle":"2024-02-20T14:14:11.973389Z","shell.execute_reply.started":"2024-02-20T14:06:37.430764Z","shell.execute_reply":"2024-02-20T14:14:11.972078Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"markdown","source":"### Conclusion and further steps\n\nThat was a simple example of how to use YOLOv8 model and more importantly, how to prepare your dataset.\n\nWhat can be done to improve the results:\n- Eplore the influence of train-test split (uneven distribution of data types)\n- Data augmentation (YOLOv8 has already built-in augmentation, but this may not be the best option for the particular cases) \n- Train hyperparameters tuning\n\n#### Remove all output files to be able to make submission","metadata":{}},{"cell_type":"code","source":"downloaded_yolo = os.path.join(os.getcwd(),'yolov8n.pt')\n\nshutil.rmtree(img_dir)\nshutil.rmtree(label_dir)\nshutil.rmtree(test_images)\nos.remove(path_to_yaml)\nos.remove(downloaded_yolo)","metadata":{},"execution_count":null,"outputs":[]}]}