{"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":"## Vinbigdata Chest X-ray Abnormalities Detection\n### Team 'scumed': 3rd Place Solution (Class 10 Replacement Component; Inference)\n\nRequired input datasets:\n- `aortic_specialist_weights` -- our trained Yolov5 weights, whose class 10 (Pleural Effusion) are extracted and used in our final solution.\n- `vinbigdata-precomputed-image_dims` -- csv containing original image dimensions for each image ID\n- `vinbigdata-test-png-512-1024` -- PNG images of official competition test data, resized to 512 and 1024 respectively\n- `yolov5-vbd` -- Snapshot of Yolov5 commit that we used (original: https://github.com/ultralytics/yolov5/)\n\nOutput:\n- `aortic_final_fixed.csv` -- predictions for class 10 (Pleural Effusion), used in our 3rd Place solution\n\nNOTE:\n- Inference takes a long time as we used a low minimum box confidence threshold (`conf_thres`). This is to replicate our final solution. You may opt to use a higher confidence threshold (e.g. 0.001) to speed up the process with negligible decrease in IOU@0.4 performance.","metadata":{}},{"cell_type":"code","source":"import time\nfrom pathlib import Path\nfrom types import SimpleNamespace\nimport os\nimport sys\nimport pandas as pd\nimport numpy as np\nimport shutil\n\nimport cv2\nimport torch\nimport torch.backends.cudnn as cudnn\nfrom numpy import random\nimport multiprocess as mp\nfrom tqdm import tqdm\n\n# Yolov5\nsys.path.append('../input/yolov5-vbd/yolov5/')\nfrom models.experimental import attempt_load\nfrom utils.datasets import LoadStreams, LoadImages\nfrom utils.general import check_img_size, non_max_suppression, apply_classifier, scale_coords, xyxy2xywh, \\\n    strip_optimizer, set_logging, increment_path\nfrom utils.plots import plot_one_box\nfrom utils.torch_utils import select_device, load_classifier, time_synchronized\n\n# Ensemble-boxes library\n!pip install -qq ensemble-boxes\nfrom ensemble_boxes import *","metadata":{"_uuid":"8f2839f25d086af736a60e9eeb907d3b93b6e0e5","_cell_guid":"b1076dfc-b9ad-4769-8c92-a6c4dae69d19","trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"code","source":"!mkdir /kaggle/temp/","metadata":{"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"code","source":"# From https://github.com/ultralytics/yolov5\ndef detect(opt, save_img=False):\n    source, weights, view_img, save_txt, imgsz = opt.source, opt.weights, opt.view_img, opt.save_txt, opt.img_size\n    webcam = source.isnumeric() or source.endswith('.txt') or source.lower().startswith(\n        ('rtsp://', 'rtmp://', 'http://'))\n\n    # Directories\n    save_dir = Path(increment_path(Path(opt.project) / opt.name, exist_ok=opt.exist_ok))  # increment run\n    (save_dir / 'labels' if save_txt else save_dir).mkdir(parents=True, exist_ok=True)  # make dir\n\n    # Initialize\n    set_logging()\n    device = select_device(opt.device)\n    half = device.type != 'cpu'  # half precision only supported on CUDA\n\n    # Load model\n    model = attempt_load(weights, map_location=device)  # load FP32 model\n    imgsz = check_img_size(imgsz, s=model.stride.max())  # check img_size\n    if half:\n        model.half()  # to FP16\n\n    # Second-stage classifier\n    classify = False\n#     if classify:\n#         modelc = load_classifier(name='resnet101', n=2)  # initialize\n#         modelc.load_state_dict(torch.load('weights/resnet101.pt', map_location=device)['model']).to(device).eval()\n\n    # Set Dataloader\n    vid_path, vid_writer = None, None\n    if webcam:\n        view_img = True\n        cudnn.benchmark = True  # set True to speed up constant image size inference\n        dataset = LoadStreams(source, img_size=imgsz)\n    else:\n        save_img = True\n        dataset = LoadImages(source, img_size=imgsz)\n\n    # Get names and colors\n    names = model.module.names if hasattr(model, 'module') else model.names\n    colors = [[random.randint(0, 255) for _ in range(3)] for _ in names]\n\n    # Run inference\n    t0 = time.time()\n    img = torch.zeros((1, 3, imgsz, imgsz), device=device)  # init img\n    _ = model(img.half() if half else img) if device.type != 'cpu' else None  # run once\n    for path, img, im0s, vid_cap in dataset:\n        img = torch.from_numpy(img).to(device)\n        img = img.half() if half else img.float()  # uint8 to fp16/32\n        img /= 255.0  # 0 - 255 to 0.0 - 1.0\n        if img.ndimension() == 3:\n            img = img.unsqueeze(0)\n\n        # Inference\n        t1 = time_synchronized()\n        pred = model(img, augment=opt.augment)[0]\n\n        # Apply NMS\n        pred = non_max_suppression(pred, opt.conf_thres, opt.iou_thres, classes=opt.classes, agnostic=opt.agnostic_nms)\n        t2 = time_synchronized()\n\n        # Apply Classifier\n        if classify:\n            pred = apply_classifier(pred, modelc, img, im0s)\n\n        # Process detections\n        for i, det in enumerate(pred):  # detections per image\n            if webcam:  # batch_size >= 1\n                p, s, im0 = Path(path[i]), '%g: ' % i, im0s[i].copy()\n            else:\n                p, s, im0 = Path(path), '', im0s\n\n            save_path = str(save_dir / p.name)\n            txt_path = str(save_dir / 'labels' / p.stem) + ('_%g' % dataset.frame if dataset.mode == 'video' else '')\n            s += '%gx%g ' % img.shape[2:]  # print string\n            gn = torch.tensor(im0.shape)[[1, 0, 1, 0]]  # normalization gain whwh\n            if len(det):\n                # Rescale boxes from img_size to im0 size\n                det[:, :4] = scale_coords(img.shape[2:], det[:, :4], im0.shape).round()\n\n                # Print results\n                for c in det[:, -1].unique():\n                    n = (det[:, -1] == c).sum()  # detections per class\n                    s += '%g %ss, ' % (n, names[int(c)])  # add to string\n\n                # Write results\n                for *xyxy, conf, cls in reversed(det):\n                    if save_txt:  # Write to file\n                        xywh = (xyxy2xywh(torch.tensor(xyxy).view(1, 4)) / gn).view(-1).tolist()  # normalized xywh\n                        line = (cls, *xywh, conf) if opt.save_conf else (cls, *xywh)  # label format\n                        with open(txt_path + '.txt', 'a') as f:\n                            f.write(('%g ' * len(line)).rstrip() % line + '\\n')\n\n                    if save_img or view_img:  # Add bbox to image\n                        label = '%s %.2f' % (names[int(cls)], conf)\n                        plot_one_box(xyxy, im0, label=label, color=colors[int(cls)], line_thickness=3)\n\n            # Print time (inference + NMS)\n            print('%sDone. (%.3fs)' % (s, t2 - t1))\n\n            # Stream results\n            if view_img:\n                cv2.imshow(str(p), im0)\n                if cv2.waitKey(1) == ord('q'):  # q to quit\n                    raise StopIteration\n\n            # Save results (image with detections)\n            if save_img:\n                if dataset.mode == 'images':\n                    cv2.imwrite(save_path, im0)\n                else:\n                    if vid_path != save_path:  # new video\n                        vid_path = save_path\n                        if isinstance(vid_writer, cv2.VideoWriter):\n                            vid_writer.release()  # release previous video writer\n\n                        fourcc = 'mp4v'  # output video codec\n                        fps = vid_cap.get(cv2.CAP_PROP_FPS)\n                        w = int(vid_cap.get(cv2.CAP_PROP_FRAME_WIDTH))\n                        h = int(vid_cap.get(cv2.CAP_PROP_FRAME_HEIGHT))\n                        vid_writer = cv2.VideoWriter(save_path, cv2.VideoWriter_fourcc(*fourcc), fps, (w, h))\n                    vid_writer.write(im0)\n\n    if save_txt or save_img:\n        s = f\"\\n{len(list(save_dir.glob('labels/*.txt')))} labels saved to {save_dir / 'labels'}\" if save_txt else ''\n        print(f\"Results saved to {save_dir}{s}\")\n\n    print('Done. (%.3fs)' % (time.time() - t0))","metadata":{"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"code","source":"# Some utility functions\ndef get_yolo_inference_df(yolo_pred_path, meta):\n\n    output = os.listdir(f'{yolo_pred_path}/labels/')\n    output = [x for x in output if x.split('.')[-1] == 'txt']\n\n    yolo_results = []\n\n    for i in output:\n        yolo_dat = pd.read_csv(f'{yolo_pred_path}/labels/{i}', sep = \" \", header = None)\n        yolo_dat.columns = ['yolo_class', 'x_center_norm', 'y_center_norm', 'width_norm', 'height_norm', 'confidence']\n        yolo_dat['image_id'] = i.split('.')[0]\n\n        yolo_results.append(yolo_dat)\n\n    yolo_dat = pd.concat(yolo_results)\n\n    yolo_dat = yolo_dat.merge(meta, how = 'left', on = 'image_id')\n\n    yolo_dat['x_min_norm'] = yolo_dat['x_center_norm'] - yolo_dat['width_norm'] / 2\n    yolo_dat['x_max_norm'] = yolo_dat['x_center_norm'] + yolo_dat['width_norm'] / 2\n    yolo_dat['y_min_norm'] = yolo_dat['y_center_norm'] - yolo_dat['height_norm'] / 2\n    yolo_dat['y_max_norm'] = yolo_dat['y_center_norm'] + yolo_dat['height_norm'] / 2\n\n    yolo_dat['x_min_norm'] = yolo_dat['x_min_norm'].apply(lambda x: max(x, 0))\n    yolo_dat['y_min_norm'] = yolo_dat['y_min_norm'].apply(lambda x: max(x, 0))\n    yolo_dat['x_max_norm'] = yolo_dat['x_max_norm'].apply(lambda x: min(x, 1))\n    yolo_dat['y_max_norm'] = yolo_dat['y_max_norm'].apply(lambda x: min(x, 1))\n\n    yolo_dat['x_min'] = (yolo_dat['x_min_norm'] * yolo_dat['width']).astype(int)\n    yolo_dat['x_max'] = (yolo_dat['x_max_norm'] * yolo_dat['width']).astype(int)\n    yolo_dat['y_min'] = (yolo_dat['y_min_norm'] * yolo_dat['height']).astype(int)\n    yolo_dat['y_max'] = (yolo_dat['y_max_norm'] * yolo_dat['height']).astype(int)\n\n    return yolo_dat\n\n\ndef wbf_ensemble(image_id, rad1, rad2, iou_thr = 0.5, skip_box_thr = 1e-10, sigma = 0.1):\n\n    rad1 = rad1.loc[rad1['image_id'] == image_id].reset_index(drop = True)\n    rad2 = rad2.loc[rad2['image_id'] == image_id].reset_index(drop = True)\n\n    rad1_boxes = np.array(rad1[['x_min_norm', 'y_min_norm', 'x_max_norm', 'y_max_norm']]).tolist()\n    rad2_boxes = np.array(rad2[['x_min_norm', 'y_min_norm', 'x_max_norm', 'y_max_norm']]).tolist()\n\n    rad1_conf = np.array(rad1['confidence']).tolist()\n    rad2_conf = np.array(rad2['confidence']).tolist()\n\n    rad1_lab = np.array(rad1['yolo_class']).tolist()\n    rad2_lab = np.array(rad2['yolo_class']).tolist()\n\n    boxes_list = [rad1_boxes, rad2_boxes]\n    scores_list = [rad1_conf, rad2_conf]\n    labels_list = [rad1_lab, rad2_lab]\n\n    weights = [1, 1]\n    boxes, scores, labels = weighted_boxes_fusion(boxes_list, scores_list, labels_list,\n                                                  weights = weights,\n                                                  iou_thr = iou_thr,\n                                                  skip_box_thr = skip_box_thr)\n\n    ret_df = pd.DataFrame.from_records(boxes, columns = ['x_min_norm', 'y_min_norm', 'x_max_norm', 'y_max_norm'])\n    ret_df['image_id'] = image_id\n    ret_df['yolo_class'] = labels\n    ret_df['confidence'] = scores\n\n    return ret_df","metadata":{"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"code","source":"print('Yolov5 Inference for 512 R8 Model...')\n\nif os.path.isdir('/kaggle/temp/R8_512'):\n    shutil.rmtree(os.path.join('/kaggle/temp/R8_512'))\n\nargs = {\n    'weights' : f'../input/aortic-specialist-weights/weights/best_R8_512.pt',\n    'source': '../input/vinbigdata-test-png-512-1024/extracted_images/512/test/',\n    'img_size': 640,\n    'conf_thres': 0.00001,\n    'iou_thres': 0.5,\n    'device': '',\n    'classes': None,\n    'view_img': False,\n    'save_txt': True,\n    'save_conf': True,\n    'agnostic_nms': False,\n    'augment': True,\n    'update': False,\n    'project': '/kaggle/temp/',\n    'name': 'R8_512',\n    'exist_ok': True\n}\n\nopt = SimpleNamespace(**args)\ndetect(opt)\n\nprint('Yolov5 Inference for 1024 R8 Model...')\nif os.path.isdir('/kaggle/temp/R8_1024'):\n    shutil.rmtree(os.path.join('/kaggle/temp/R8_1024'))\n\nargs = {\n    'weights' : f'../input/aortic-specialist-weights/weights/best_R8_1024.pt',\n    'source': '../input/vinbigdata-test-png-512-1024/extracted_images/1024/test/',\n    'img_size': 1344,\n    'conf_thres': 0.00001,\n    'iou_thres': 0.5,\n    'device': '',\n    'classes': None,\n    'view_img': False,\n    'save_txt': True,\n    'save_conf': True,\n    'agnostic_nms': False,\n    'augment': True,\n    'update': False,\n    'project': '/kaggle/temp/',\n    'name': 'R8_1024',\n    'exist_ok': True\n}\n\nopt = SimpleNamespace(**args)\ndetect(opt)\n\nmetadata = pd.read_csv('../input/vinbigdata-precomputed-image-dims/test_meta.csv')\nyolo_dat_512 = get_yolo_inference_df('/kaggle/temp/R8_512', metadata)\nyolo_dat_1024 = get_yolo_inference_df('/kaggle/temp/R8_1024', metadata)\n\nall_imageids = np.unique(np.concatenate([yolo_dat_512.image_id, yolo_dat_1024.image_id]))\n\npool = mp.Pool(min(mp.cpu_count(), 2))\n\ndef wbf_ensemble_wrap(i):\n\n    return wbf_ensemble(all_imageids[i],\n                        yolo_dat_512,\n                        yolo_dat_1024,\n                        iou_thr=0.6)\n\nwith pool as p:\n    res = list(tqdm(p.imap(wbf_ensemble_wrap,\n                           range(len(all_imageids))),\n                           total = len(all_imageids)))\n\npool.terminate()\npool.join()\n\nyolo_dat = pd.concat(res).reset_index(drop = True)\nyolo_dat['yolo_class'] = yolo_dat['yolo_class'].astype(int)\n\nyolo_dat = yolo_dat.merge(metadata, how = 'left', on = 'image_id')\n#yolo_dat.rename(columns = {'dim0': 'height', 'dim1': 'width'}, inplace = True)\n\nyolo_dat['x_min'] = (yolo_dat['x_min_norm'] * yolo_dat['width']).astype(int)\nyolo_dat['x_max'] = (yolo_dat['x_max_norm'] * yolo_dat['width']).astype(int)\nyolo_dat['y_min'] = (yolo_dat['y_min_norm'] * yolo_dat['height']).astype(int)\nyolo_dat['y_max'] = (yolo_dat['y_max_norm'] * yolo_dat['height']).astype(int)\n\n# In our final solution, only class 10 is used from this detector\nyolo_dat = yolo_dat.loc[yolo_dat['yolo_class'] == 10]\nyolo_dat.to_csv('final_cls10.csv', index = False)\n","metadata":{"trusted":true},"execution_count":null,"outputs":[]}]}