{"cells":[{"metadata":{"trusted":true},"cell_type":"code","source":"!pip install --no-deps '../input/pycocotools/pycocotools-2.0-cp37-cp37m-linux_x86_64.whl' > /dev/null","execution_count":null,"outputs":[]},{"metadata":{"trusted":true},"cell_type":"code","source":"!pip install --no-deps '../input/timm0130/timm-0.1.30-py3-none-any.whl' > /dev/null","execution_count":null,"outputs":[]},{"metadata":{"_uuid":"8f2839f25d086af736a60e9eeb907d3b93b6e0e5","_cell_guid":"b1076dfc-b9ad-4769-8c92-a6c4dae69d19","trusted":true},"cell_type":"code","source":"import sys\nimport os\nsys.path.insert(0, \"../input/timm-efficientdet-pytorch-v1-3-0/efficientdet-pytorch-master\")\nsys.path.insert(0, \"../input/omegaconf\")\nsys.path.insert(0, \"../input/weightedboxesfusion\")\n\n\n# You can write up to 5GB 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","execution_count":null,"outputs":[]},{"metadata":{"trusted":true},"cell_type":"code","source":"import cv2\nimport numpy as np\nimport pandas as pd\nfrom matplotlib import pyplot as plt\nfrom sklearn.model_selection import train_test_split\nimport torch.utils as utils\nfrom torch.utils.data import Dataset, DataLoader\nimport torch.nn as nn\nimport torch.nn.functional as F\nimport torch\nimport torchvision\nfrom torchvision.models.detection.faster_rcnn import FastRCNNPredictor\nimport torch.optim\nimport gc\nfrom tqdm.auto import tqdm\nfrom tqdm import tqdm_notebook\nimport glob\nimport random\nimport time\nfrom datetime import datetime\nimport ensemble_boxes\nfrom ensemble_boxes import *\nfrom itertools import product\n%matplotlib inline\nfrom albumentations.pytorch.transforms import ToTensorV2\nfrom sklearn.model_selection import StratifiedKFold\nfrom torch.utils.data.sampler import SequentialSampler, RandomSampler\nfrom albumentations import (Blur, MotionBlur, MedianBlur, GaussianBlur,\n                            VerticalFlip, HorizontalFlip, IAASharpen,\n                            OneOf, Compose , BboxParams, Resize, HueSaturationValue\n                            ,RandomBrightnessContrast, ToGray , Cutout ,  RandomSizedCrop)","execution_count":null,"outputs":[]},{"metadata":{"trusted":true},"cell_type":"code","source":"from effdet import get_efficientdet_config, EfficientDet, DetBenchTrain , DetBenchPredict\nfrom effdet.efficientdet import HeadNet","execution_count":null,"outputs":[]},{"metadata":{},"cell_type":"markdown","source":"**Path directories**","execution_count":null},{"metadata":{"trusted":true},"cell_type":"code","source":"DIR_PATH = '../input/global-wheat-detection/'\ndir = glob.glob(os.path.join(DIR_PATH , '*'))\ndir.sort(reverse=True)\nprint(dir)\ntrain_paths = glob.glob(os.path.join(dir[1] , '*'))\ntest_paths = glob.glob(os.path.join(dir[2] , '*'))","execution_count":null,"outputs":[]},{"metadata":{"trusted":true},"cell_type":"code","source":"df = pd.read_csv(dir[0])\nbboxs = np.stack(df['bbox'].apply(lambda x: np.fromstring(x[1:-1], sep=',')))\nfor i, column in enumerate(['x', 'y', 'w', 'h']):\n    df[column] = bboxs[:,i]\ndf.drop(columns=['bbox'], inplace=True)\ndf","execution_count":null,"outputs":[]},{"metadata":{"trusted":true},"cell_type":"code","source":"skf = StratifiedKFold(n_splits=5, shuffle=True, random_state=42)\n\ndf_folds = df[['image_id']].copy()\ndf_folds.loc[:, 'bbox_count'] = 1\ndf_folds = df_folds.groupby('image_id').count()\ndf_folds.loc[:, 'source'] = df[['image_id', 'source']].groupby('image_id').min()['source']\ndf_folds.loc[:, 'stratify_group'] = np.char.add(\n    df_folds['source'].values.astype(str),\n    df_folds['bbox_count'].apply(lambda x: f'_{x // 15}').values.astype(str)\n)\ndf_folds.loc[:, 'fold'] = 0\nfor fold_number, (train_index, val_index) in enumerate(skf.split(X=df_folds.index, y=df_folds['stratify_group'])):\n    df_folds.loc[df_folds.iloc[val_index].index, 'fold'] = fold_number\n\ndf_folds","execution_count":null,"outputs":[]},{"metadata":{"trusted":true},"cell_type":"code","source":"df_folds.groupby('fold')['bbox_count'].mean()","execution_count":null,"outputs":[]},{"metadata":{},"cell_type":"markdown","source":"# **Help functions**","execution_count":null},{"metadata":{"trusted":true},"cell_type":"code","source":"def show_image(image, boxes, title):\n  fig, ax = plt.subplots(1, 1, figsize=(25, 8))\n  boxes = boxes.astype(np.int32)\n  for box in boxes:\n      cv2.rectangle(image, (box[0], box[1]), (box[2],  box[3]), (1, 1, 0), 3)\n  ax.set_title(title) \n  ax.set_axis_off()\n  ax.imshow(image);\n\n\ndef load_image_and_boxes(image_path):\n  image_id = image_path.split('/')[-1].split('.')[0]\n  image = cv2.imread(image_path, cv2.IMREAD_COLOR)\n  image = cv2.cvtColor(image, cv2.COLOR_BGR2RGB).astype(np.float32)\n  image /= 255.0\n  records = df[df['image_id'] == image_id]\n  boxes = records[['x', 'y', 'w', 'h']].values\n  boxes[:, 2] = boxes[:, 0] + boxes[:, 2]\n  boxes[:, 3] = boxes[:, 1] + boxes[:, 3]\n  return image, boxes\n\n\ndef images_after_augmentation(original_image, boxes, augmentation):\n    aug_image = original_image.copy()\n    boxes = boxes.astype(np.int32)\n    if isinstance(augmentation , VerticalFlip) or isinstance(augmentation , HorizontalFlip):\n      for box in boxes:\n          cv2.rectangle(aug_image, (box[0], box[1]), (box[2],  box[3]), (1, 1, 0), 2)\n      sample = {'image': aug_image, 'label': 'label'}\n      compose = Compose([augmentation], p=1)\n      aug_image = compose(**sample)['image']\n    else:\n      sample = {'image': aug_image, 'label': 'label'}\n      compose = Compose([augmentation], p=1)\n      aug_image = compose(**sample)['image']\n      for box in boxes:\n          cv2.rectangle(aug_image, (box[0], box[1]), (box[2],  box[3]), (1, 1, 0), 2)\n    plt.figure(figsize=[12, 12])\n    for i in range(len([original_image, aug_image])):\n            image = [original_image, aug_image][i]\n            plt.subplot(1, 2, i+1)\n            plt.title(['Original Image', 'After Augmentaion'][i])\n            plt.axis(\"off\")\n            plt.imshow(image)\n    plt.show()\n\n\n# Functions to visualize bounding boxes and class labels on an image. \n# Based on https://github.com/facebookresearch/Detectron/blob/master/detectron/utils/vis.py\ndef visualize(**images):\n    \"\"\"PLot images in one row.\"\"\"\n    n = len(images)\n    plt.figure(figsize=(16, 5))\n    for i, (name, image) in enumerate(images.items()):\n        plt.subplot(1, n, i + 1)\n        plt.xticks([])\n        plt.yticks([])\n        plt.title(' '.join(name.split('_')).title())\n        plt.imshow(image)\n    plt.show()\n\n\nBOX_COLOR = (255, 255, 0)\n\n\ndef visualize_bbox(img, bbox, color=BOX_COLOR, thickness=3):\n    xmin, ymin, xmax, ymax = bbox\n    xmin, ymin, xmax, ymax =  int(xmin), int(ymin), int(xmax), int(ymax)\n    cv2.rectangle(img, (xmin , ymin), (xmax, ymax), color=BOX_COLOR, thickness=thickness)\n    return img\n\ndef visualizeTarget(image, target , visualize_data_loader = True):\n  boxes = target['boxes']\n  if visualize_data_loader:\n    if not type(boxes).__module__ == np.__name__:\n      boxes = boxes.numpy()\n    image = image.numpy()\n    image = np.transpose(image,(1,2,0))\n  img = image.copy()\n  for idx, bbox in enumerate(boxes):\n      img = visualize_bbox(img, bbox)\n  return img","execution_count":null,"outputs":[]},{"metadata":{},"cell_type":"markdown","source":"# **Examples**","execution_count":null},{"metadata":{"trusted":true},"cell_type":"code","source":"image_id = '8425a537b.jpg'\nimage_path = glob.glob(os.path.join(dir[1] , image_id))\nimage , boxes  = load_image_and_boxes(image_path[0])\nshow_image(image, boxes, \"Image without bounding box\")","execution_count":null,"outputs":[]},{"metadata":{"trusted":true},"cell_type":"code","source":"image_id = 'b3c96d5ad.jpg'\nimage_path = glob.glob(os.path.join(dir[1] , image_id))\nimage , boxes  = load_image_and_boxes(image_path[0])\nshow_image(image, boxes, \"Image with bounding box\")","execution_count":null,"outputs":[]},{"metadata":{},"cell_type":"markdown","source":"**Examples of some augmentations that we will use for our train**","execution_count":null},{"metadata":{},"cell_type":"markdown","source":"**Blur**","execution_count":null},{"metadata":{"trusted":true},"cell_type":"code","source":"images_after_augmentation(image, boxes, Blur(blur_limit=7 ,p=1))","execution_count":null,"outputs":[]},{"metadata":{},"cell_type":"markdown","source":"# **Vertical Flip**","execution_count":null},{"metadata":{"trusted":true},"cell_type":"code","source":"images_after_augmentation(image, boxes, VerticalFlip(p=1))","execution_count":null,"outputs":[]},{"metadata":{},"cell_type":"markdown","source":"# **Horizontal Flip**","execution_count":null},{"metadata":{"trusted":true},"cell_type":"code","source":"images_after_augmentation(image, boxes, HorizontalFlip(p=1))","execution_count":null,"outputs":[]},{"metadata":{},"cell_type":"markdown","source":"# **Wheat dataset**","execution_count":null},{"metadata":{"trusted":true},"cell_type":"code","source":"class WheatDataset(Dataset):\n    def __init__(self , dataframe , image_ids,  transforms = None):\n        super().__init__()\n        self.image_ids = image_ids\n        self.dataframe = dataframe\n        self.transforms = transforms\n\n\n    def __getitem__(self, index: int):\n        image_id = self.image_ids[index]\n    \n        image, boxes = self.load_image_and_boxes(index)\n        labels = torch.ones((boxes.shape[0],), dtype=torch.int64)\n \n        target = {}\n        target['boxes'] = boxes\n        target['labels'] = labels\n        target['image_id'] = torch.tensor([index])\n    \n        if self.transforms:\n            for i in range(10):\n                sample = self.transforms(**{\n                        'image': image,\n                        'bboxes': target['boxes'],\n                        'labels': labels\n                })\n                if len(sample['bboxes']) > 0:\n                    image = sample['image']\n                    target['boxes'] = torch.stack(tuple(map(torch.tensor, zip(*sample['bboxes'])))).permute(1, 0)\n                    target['labels'] = torch.stack(sample['labels'])\n                    break\n        return image, target, image_id\n              \n      \n    def __len__(self) -> int:\n        return self.image_ids.shape[0]\n\n\n    def load_image_and_boxes(self, index):\n        image_id = self.image_ids[index]\n        image = cv2.imread(f'{dir[1]}/{image_id}.jpg', cv2.IMREAD_COLOR)\n        image = cv2.cvtColor(image, cv2.COLOR_BGR2RGB).astype(np.float32)\n        image /= 255.0\n        records = self.dataframe[self.dataframe['image_id'] == image_id]\n        boxes = records[['x', 'y', 'w', 'h']].values\n        area = (boxes[:, 2] * boxes[:, 3])\n        boxes[:, 2] = boxes[:, 0] + boxes[:, 2]\n        boxes[:, 3] = boxes[:, 1] + boxes[:, 3]\n        return image, boxes","execution_count":null,"outputs":[]},{"metadata":{},"cell_type":"markdown","source":"# **Augmentations**","execution_count":null},{"metadata":{"trusted":true},"cell_type":"code","source":"transforms_train = Compose([IAASharpen(p = 0.5), RandomSizedCrop(min_max_height=(800, 800), height=1024, width=1024, p=0.5),ToGray(p=0.01),\n                            OneOf([HueSaturationValue(hue_shift_limit=0.2, sat_shift_limit= 0.2, val_shift_limit=0.2, p=0.9),\n                                    RandomBrightnessContrast(brightness_limit=0.2,contrast_limit=0.2, p=0.9)],p=0.8),\n                            OneOf([Blur(blur_limit=3), MotionBlur(blur_limit=3), MedianBlur(blur_limit=3)]),\n                            OneOf([VerticalFlip(), HorizontalFlip()]),\n                            Cutout(num_holes=10, max_h_size=64, max_w_size=64, fill_value=0, p=0.5),\n                            Resize(height=512, width=512, p = 1.0)\n                            ,ToTensorV2(p=1.0),\n                            ],p = 1.0,bbox_params=BboxParams(format='pascal_voc', min_area=0, \n                                               min_visibility=0, label_fields=['labels']))\n\n\ntransforms_valid = Compose([Resize(height=512, width=512, p=1.0),\n                            ToTensorV2(p=1.0),], p=1.0,\n                            bbox_params=BboxParams(\n                                format='pascal_voc',\n                                min_area=0,\n                                min_visibility=0,\n                                label_fields=['labels']))","execution_count":null,"outputs":[]},{"metadata":{},"cell_type":"markdown","source":"# **Data Loader**","execution_count":null},{"metadata":{"trusted":true},"cell_type":"code","source":"def collate_fn(batch):\n    return tuple(zip(*batch))","execution_count":null,"outputs":[]},{"metadata":{"trusted":true},"cell_type":"code","source":"bs = 2\nfold_number = 0\n\ntrain_set = WheatDataset(dataframe=df, image_ids=df_folds[df_folds['fold'] != fold_number].index.values , transforms=transforms_train)\nvalid_set = WheatDataset(dataframe=df, image_ids=df_folds[df_folds['fold'] == fold_number].index.values , transforms=transforms_valid)\n\ntrain_loader = DataLoader(train_set,batch_size = bs,sampler=RandomSampler(train_set),pin_memory=False,\n        drop_last=True,collate_fn=collate_fn , num_workers=2)\nvalid_loader = DataLoader(valid_set,batch_size = bs ,sampler=SequentialSampler(valid_set),pin_memory=False,\n        drop_last=True,collate_fn=collate_fn , num_workers=2)\n\nimages , targets , path_images = next(iter(train_loader))\nimg = visualizeTarget(images[0],targets[0])\nvisualize(Example_one_image_from_dataloader = img)","execution_count":null,"outputs":[]},{"metadata":{},"cell_type":"markdown","source":"# **Train/Validation**","execution_count":null},{"metadata":{"trusted":true},"cell_type":"code","source":"class Calculator(object):\n    def __init__(self):\n        self.reset()\n\n    def reset(self):\n        self.val = 0\n        self.avg = 0\n        self.sum = 0\n        self.count = 0\n\n    def update(self, val, n=1):\n        self.val = val\n        self.sum += val * n\n        self.count += n\n        self.avg = self.sum / self.count","execution_count":null,"outputs":[]},{"metadata":{"trusted":true},"cell_type":"code","source":"class Training:\n    \n    def __init__(self, model, device, config):\n        self.config = config\n        self.epoch = 0\n\n        self.base_dir = f'{config.folder}'\n        if not os.path.exists(self.base_dir):\n            os.makedirs(self.base_dir)\n        \n        self.best_calc_loss = 10**5\n\n        self.model = model\n        self.device = device\n\n\n        self.optimizer = torch.optim.AdamW(self.model.parameters(), lr=config.lr)\n        self.scheduler = config.SchedulerClass(self.optimizer, **config.scheduler_params)\n\n\n    def train_loop(self, train_loader, validation_loader):\n        for e in range(self.config.n_epochs):\n            if self.config.verbose:\n                lr = self.optimizer.param_groups[0]['lr']\n                timestamp = datetime.utcnow().isoformat()\n                print(f'\\n{timestamp}\\nLR: {lr}')\n\n            t = time.time()\n            calc_loss = self.train_one_epoch(train_loader)\n\n            print(f'Train. Epoch: {self.epoch}, Loss: {calc_loss.avg:.5f}, time: {(time.time() - t):.5f}')\n            self.save_model(f'{self.base_dir}/last-epoch.bin')\n\n            t = time.time()\n            calc_loss = self.valid_one_epoch(validation_loader)\n\n            print(f'Val. Epoch: {self.epoch}, Loss: {calc_loss.avg:.5f}, time: {(time.time() - t):.5f}')\n            if calc_loss.avg < self.best_calc_loss:\n                self.best_calc_loss = calc_loss.avg\n                self.model.eval()\n                self.save_model(f'{self.base_dir}/best-model-{str(self.epoch).zfill(3)}epoch.bin')\n\n            if self.config.validation_scheduler:\n                self.scheduler.step(metrics=calc_loss.avg)\n\n            self.epoch += 1\n\n\n    def train_one_epoch(self, train_loader):\n        self.model.train()\n        calc_loss = Calculator()\n        t = time.time()\n        target_res = {}\n        for images, targets, image_ids in tqdm_notebook(train_loader):\n            images = torch.stack(images)\n            images = images.to(self.device).float()\n            batch_size = images.shape[0]\n\n            boxes = [target['boxes'].to(self.device).float() for target in targets]\n            labels = [target['labels'].to(self.device).float() for target in targets]\n\n            target_res['bbox'] = boxes\n            target_res['cls'] = labels  \n\n            self.optimizer.zero_grad()\n\n            outputs = self.model(images, target_res)\n            loss = outputs['loss']\n            calc_loss.update(loss.detach().item(), batch_size)\n        \n            loss.backward()\n            self.optimizer.step()\n\n            if self.config.step_scheduler:  \n                self.scheduler.step()\n      \n        return calc_loss\n\n\n    \n    def valid_one_epoch(self, val_loader):\n        self.model.eval()\n        calc_loss = Calculator()\n        t = time.time()\n        \n        with torch.no_grad():\n            for images, targets, image_ids in tqdm_notebook(val_loader):\n                images = torch.stack(images)\n                images = images.to(self.device).float()\n                batch_size = images.shape[0]\n                boxes = [target['boxes'].to(self.device).float() for target in targets]\n                labels = [target['labels'].to(self.device).float() for target in targets]\n\n                target_res = {}\n                target_res['bbox'] = boxes\n                target_res['cls'] = labels \n                target_res[\"img_scale\"] = torch.tensor([1.0] * batch_size, dtype=torch.float).to(self.device)\n                target_res[\"img_size\"] = torch.tensor([images[0].shape[-2:]] * batch_size, dtype=torch.float).to(self.device)\n        \n                outputs = self.model(images, target_res)\n                loss = outputs['loss']\n                calc_loss.update(loss.detach().item(), batch_size)\n        return calc_loss\n\n\n    def save_model(self, path):\n        self.model.eval()\n        torch.save({\n            'model_state_dict': self.model.state_dict(),\n            'optimizer_state_dict': self.optimizer.state_dict(),\n            'scheduler_state_dict': self.scheduler.state_dict(),\n            'best_calc_loss': self.best_calc_loss,\n            'epoch': self.epoch,\n        }, path)\n\n\n    def load_model(self, path):\n        model = torch.load(path)\n        self.model.load_state_dict(model['model_state_dict'])\n        self.optimizer.load_state_dict(model['optimizer_state_dict'])\n        self.scheduler.load_state_dict(model['scheduler_state_dict'])\n        self.best_calc_loss = model['best_calc_loss']\n        self.epoch = model['epoch'] + 1","execution_count":null,"outputs":[]},{"metadata":{},"cell_type":"markdown","source":"# **EfficientDet_D5 Model**\n`num_classes = 1` - only one class called wheat","execution_count":null},{"metadata":{"trusted":true},"cell_type":"code","source":"def get_model(num_classes = 1):\n    config = get_efficientdet_config('tf_efficientdet_d5')\n    model = EfficientDet(config, pretrained_backbone=False)\n    checkpoint = torch.load('/content/drive/My Drive/Global Wheat Detection/efficientdet_d7/efficientdet_d5-ef44aea8.pth')\n    model.load_state_dict(checkpoint)\n    config.num_classes = num_classes\n    config.image_size = 512\n    model.class_net = HeadNet(config, num_outputs=config.num_classes, norm_kwargs=dict(eps=.001, momentum=.01))\n    return DetBenchTrain(model, config)","execution_count":null,"outputs":[]},{"metadata":{"trusted":true},"cell_type":"code","source":"class GlobalParametersTrain:\n    lr = 0.0002 \n    n_epochs = 40 \n\n    folder = '/content/drive/My Drive/Global Wheat Detection/efficientdet_d5'\n\n    verbose = True\n    verbose_step = 10\n\n    step_scheduler = False \n    validation_scheduler = True \n    \n    SchedulerClass = torch.optim.lr_scheduler.ReduceLROnPlateau\n    scheduler_params = dict(mode='min',factor=0.5,patience=1,verbose=False, threshold=0.0001,threshold_mode='abs',cooldown=0, min_lr=1e-8,eps=1e-08)","execution_count":null,"outputs":[]},{"metadata":{},"cell_type":"markdown","source":"**Load best efficientdet d5 model are trained**","execution_count":null},{"metadata":{"trusted":true},"cell_type":"code","source":"def load_model(checkpoint_path):\n    config = get_efficientdet_config('tf_efficientdet_d5')\n    model = EfficientDet(config, pretrained_backbone=False)\n\n    config.num_classes = 1\n    config.image_size=512\n    model.class_net = HeadNet(config, num_outputs=config.num_classes, norm_kwargs=dict(eps=.001, momentum=.01))\n\n    checkpoint = torch.load(checkpoint_path)\n    model.load_state_dict(checkpoint['model_state_dict'])\n\n    del checkpoint\n    gc.collect()\n\n    model = DetBenchPredict(model, config)\n    model.eval();\n    return model.cuda()\n\nmodel = load_model('../input/efficientdetd5model17e/efficient-best-017epoch.bin')","execution_count":null,"outputs":[]},{"metadata":{"trusted":true},"cell_type":"code","source":"device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')\n\ndef make_predictions(images ,  score_threshold=0.22):\n    images = torch.stack(images).cuda().float()\n    predictions = []\n    with torch.no_grad():\n        outputs = model(images , torch.tensor([1.0] * images.shape[0], dtype=torch.float).to(device) , torch.tensor([images[0].shape[-2:]] * images.shape[0], dtype=torch.float).to(device))\n        for i in range(images.shape[0]):\n            boxes = outputs[i].detach().cpu().numpy()[:,:4]    \n            scores = outputs[i].detach().cpu().numpy()[:,4]\n            labels = outputs[i].detach().cpu().numpy()[:,5]\n            indexes = np.where(scores > score_threshold)[0]\n            boxes = boxes[indexes]\n            boxes[:, 2] = boxes[:, 2] + boxes[:, 0]\n            boxes[:, 3] = boxes[:, 3] + boxes[:, 1]\n            predictions.append({\n                'boxes': boxes[indexes],\n                'scores': scores[indexes],\n                'labels': labels[indexes],\n            })\n    return [predictions]","execution_count":null,"outputs":[]},{"metadata":{},"cell_type":"markdown","source":"# **Weighted Boxes Fusion**\n**Why WBF can be better than NMS or SoftNMS?**\n\nBoth NMS and Soft-NMS exclude some boxes, but WBF uses information from all boxes. It can fix some cases where all boxes are predicted inaccurate by all models. NMS will leave only one inaccurate box, while WBF will fix it using information from all 3 boxes.\n\nSee the example in Fig. 1 , red predictions, blue ground truth.\n\nReference : https://arxiv.org/pdf/1910.13302.pdf","execution_count":null},{"metadata":{},"cell_type":"markdown","source":"![image.png](data:image/png;base64,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)","execution_count":null},{"metadata":{"trusted":true},"cell_type":"code","source":"def run_wbf(predictions, image_index, image_size=512, iou_thr=0.44, skip_box_thr=0.43, weights=None):\n    boxes = [(prediction[image_index]['boxes']/(image_size-1)).tolist()  for prediction in predictions]\n    scores = [prediction[image_index]['scores'].tolist()  for prediction in predictions]\n    # labels = [np.ones(prediction[image_index]['scores'].shape[0]).tolist() for prediction in predictions]\n    labels = [prediction[image_index]['labels'].tolist() for prediction in predictions]\n    boxes, scores, labels = weighted_boxes_fusion(boxes, scores, labels, weights=None, iou_thr=iou_thr, skip_box_thr=skip_box_thr)\n    boxes = boxes*(image_size-1)\n    return boxes, scores, labels","execution_count":null,"outputs":[]},{"metadata":{"trusted":true},"cell_type":"code","source":"for j, (images, targets , image_ids) in enumerate(valid_loader):\n    break\npredictions = make_predictions(images)\n\ni = 0\nsample = images[i].permute(1,2,0).cpu().numpy()\n\nboxes, scores, labels = run_wbf(predictions, image_index=i)\nboxes = boxes.astype(np.int32).clip(min=0, max=511)","execution_count":null,"outputs":[]},{"metadata":{},"cell_type":"markdown","source":"**Example from validation set of ground truth vs prediction**","execution_count":null},{"metadata":{"trusted":true},"cell_type":"code","source":"def show_result(sample_id, preds, gt_boxes):\n    sample = cv2.imread(f'{dir[1]}/{sample_id}.jpg', cv2.IMREAD_COLOR)\n    sample = cv2.cvtColor(sample, cv2.COLOR_BGR2RGB)\n\n    preds = (preds*2).astype(np.int32).clip(min=0, max=1023)\n    gt_boxes = (gt_boxes*2).astype(np.int32).clip(min=0, max=1023)\n    fig, ax = plt.subplots(1, 1, figsize=(16, 8))\n\n    for gt_box in gt_boxes:    \n        cv2.rectangle(\n            sample,\n            (gt_box[0], gt_box[1]),\n            (gt_box[2], gt_box[3]),\n            (255, 255, 0), 3\n        )\n        \n    for pred_box in preds:\n        cv2.rectangle(sample,(pred_box[0], pred_box[1]),(pred_box[2], pred_box[3]),\n            (255, 0, 0), 3)\n    ax.set_axis_off()\n    ax.imshow(sample)\n    ax.set_title(\"RED: Predicted | YELLOW - Ground-truth\")\n# f5a1f0358\nshow_result(image_ids[0], boxes, targets[0]['boxes'].numpy())","execution_count":null,"outputs":[]},{"metadata":{},"cell_type":"markdown","source":"# **TTA Classes**","execution_count":null},{"metadata":{"trusted":true},"cell_type":"code","source":"class BaseWheatTTA:\n    \"\"\" author: @shonenkov \"\"\"\n    image_size = 512\n\n    def augment(self, image):\n        raise NotImplementedError\n    \n    def batch_augment(self, images):\n        raise NotImplementedError\n    \n    def deaugment_boxes(self, boxes):\n        raise NotImplementedError\n\nclass TTAHorizontalFlip(BaseWheatTTA):\n    \"\"\" author: @shonenkov \"\"\"\n\n    def augment(self, image):\n        return image.flip(1)\n    \n    def batch_augment(self, images):\n        return images.flip(2)\n    \n    def deaugment_boxes(self, boxes):\n        boxes[:, [1,3]] = self.image_size - boxes[:, [3,1]]\n        return boxes\n\nclass TTAVerticalFlip(BaseWheatTTA):\n    \"\"\" author: @shonenkov \"\"\"\n    \n    def augment(self, image):\n        return image.flip(2)\n    \n    def batch_augment(self, images):\n        return images.flip(3)\n    \n    def deaugment_boxes(self, boxes):\n        boxes[:, [0,2]] = self.image_size - boxes[:, [2,0]]\n        return boxes\n    \nclass TTARotate90(BaseWheatTTA):\n    \"\"\" author: @shonenkov \"\"\"\n    \n    def augment(self, image):\n        return torch.rot90(image, 1, (1, 2))\n\n    def batch_augment(self, images):\n        return torch.rot90(images, 1, (2, 3))\n    \n    def deaugment_boxes(self, boxes):\n        res_boxes = boxes.copy()\n        res_boxes[:, [0,2]] = self.image_size - boxes[:, [1,3]]\n        res_boxes[:, [1,3]] = boxes[:, [2,0]]\n        return res_boxes\n\nclass TTACompose(BaseWheatTTA):\n    \"\"\" author: @shonenkov \"\"\"\n    def __init__(self, transforms):\n        self.transforms = transforms\n        \n    def augment(self, image):\n        for transform in self.transforms:\n            image = transform.augment(image)\n        return image\n    \n    def batch_augment(self, images):\n        for transform in self.transforms:\n            images = transform.batch_augment(images)\n        return images\n    \n    def prepare_boxes(self, boxes):\n        result_boxes = boxes.copy()\n        result_boxes[:,0] = np.min(boxes[:, [0,2]], axis=1)\n        result_boxes[:,2] = np.max(boxes[:, [0,2]], axis=1)\n        result_boxes[:,1] = np.min(boxes[:, [1,3]], axis=1)\n        result_boxes[:,3] = np.max(boxes[:, [1,3]], axis=1)\n        return result_boxes\n    \n    def deaugment_boxes(self, boxes):\n        for transform in self.transforms[::-1]:\n            boxes = transform.deaugment_boxes(boxes)\n        return self.prepare_boxes(boxes)","execution_count":null,"outputs":[]},{"metadata":{},"cell_type":"markdown","source":"# **Combinations of TTA**","execution_count":null},{"metadata":{"trusted":true},"cell_type":"code","source":"tta_transforms = []\nfor tta_combination in product([TTAHorizontalFlip(), None], [TTAVerticalFlip(), None],[TTARotate90(), None]):\n    tta_transforms.append(TTACompose([tta_transform for tta_transform in tta_combination if tta_transform]))","execution_count":null,"outputs":[]},{"metadata":{"trusted":true},"cell_type":"code","source":"def make_tta_predictions(images, score_threshold=0.5):\n    with torch.no_grad():\n        images = torch.stack(images).float().cuda()\n        predictions = []\n        for tta_transform in tta_transforms:\n            result = []\n            outputs = model(tta_transform.batch_augment(images.clone()), torch.tensor([1]*images.shape[0]).float().cuda() ,  torch.tensor([images[0].shape[-2:]] * images.shape[0], dtype=torch.float).to(device))\n\n            for i in range(images.shape[0]):\n                boxes = outputs[i].detach().cpu().numpy()[:,:4]    \n                scores = outputs[i].detach().cpu().numpy()[:,4]\n                labels = outputs[i].detach().cpu().numpy()[:,5]\n                indexes = np.where(scores > score_threshold)[0]\n                boxes = boxes[indexes]\n                boxes[:, 2] = boxes[:, 2] + boxes[:, 0]\n                boxes[:, 3] = boxes[:, 3] + boxes[:, 1]\n                boxes = tta_transform.deaugment_boxes(boxes.copy())\n                result.append({\n                    'boxes': boxes,\n                    'scores': scores[indexes],\n                    'labels': labels[indexes],\n                })\n            predictions.append(result)\n    return predictions","execution_count":null,"outputs":[]},{"metadata":{"trusted":true},"cell_type":"code","source":"for j, (images, targets , image_ids) in enumerate(valid_loader):\n    break\n\n\npredictions = make_tta_predictions(images)\n\ni = 0\nsample = images[i].permute(1,2,0).cpu().numpy()\n\nboxes, scores, labels = run_wbf(predictions, image_index=i)\nboxes = boxes.astype(np.int32).clip(min=0, max=511)\n\nfig, ax = plt.subplots(1, 1, figsize=(16, 8))\n\nfor box in boxes:\n    cv2.rectangle(sample, (box[0], box[1]), (box[2], box[3]), (1, 1, 0), 2)\n    \nax.set_axis_off()\nax.imshow(sample);","execution_count":null,"outputs":[]},{"metadata":{},"cell_type":"markdown","source":"# **Inference**","execution_count":null},{"metadata":{"trusted":true},"cell_type":"code","source":"test_transforms =  Compose([\n            Resize(height=512, width=512, p=1.0),\n            ToTensorV2(p=1.0),\n        ], p=1.0)","execution_count":null,"outputs":[]},{"metadata":{"trusted":true},"cell_type":"code","source":"class TestDataset(Dataset):\n\n    def __init__(self, image_ids, transforms=None):\n        super().__init__()\n        self.image_ids = image_ids\n        self.transforms = transforms\n\n    def __getitem__(self, index):\n        image_id = self.image_ids[index]\n        image = cv2.imread(f'{dir[2]}/{image_id}.jpg', cv2.IMREAD_COLOR)\n        image = cv2.cvtColor(image, cv2.COLOR_BGR2RGB).astype(np.float32)\n        image /= 255.0\n        if self.transforms:\n            sample = {'image': image}\n            sample = self.transforms(**sample)\n            image = sample['image']\n        return image, image_id\n\n    def __len__(self):\n        return self.image_ids.shape[0]","execution_count":null,"outputs":[]},{"metadata":{"trusted":true},"cell_type":"code","source":"test_set = TestDataset(image_ids=np.array([path.split('/')[-1][:-4] for path in test_paths]),transforms=test_transforms)\ntest_loader = DataLoader(test_set,batch_size=4,shuffle=False,num_workers=2,drop_last=False,collate_fn=collate_fn)","execution_count":null,"outputs":[]},{"metadata":{"trusted":true},"cell_type":"code","source":"def format_prediction_string(boxes, scores):\n    pred_strings = []\n    for j in zip(scores, boxes):\n        pred_strings.append(\"{0:.4f} {1} {2} {3} {4}\".format(j[0], j[1][0], j[1][1], j[1][2], j[1][3]))\n    return \" \".join(pred_strings)","execution_count":null,"outputs":[]},{"metadata":{"trusted":true},"cell_type":"code","source":"submission = []\n\nfor images, image_ids in test_loader:\n    predictions = make_tta_predictions(images)\n    for i, image in enumerate(images):\n        boxes, scores, labels = run_wbf(predictions, image_index=i)\n        \n        img = visualizeTarget(image, {'boxes' : boxes} , visualize_data_loader = True)\n        visualize(test_image = img)\n        \n        boxes = (boxes*2).astype(np.int32).clip(min=0, max=1023)\n        image_id = image_ids[i]\n        \n        boxes[:, 2] = boxes[:, 2] - boxes[:, 0]\n        boxes[:, 3] = boxes[:, 3] - boxes[:, 1]\n\n        result = {\n            'image_id': image_id,\n            'PredictionString': format_prediction_string(boxes, scores)\n        }\n        submission.append(result)","execution_count":null,"outputs":[]},{"metadata":{},"cell_type":"markdown","source":"# **Submission**","execution_count":null},{"metadata":{"trusted":true},"cell_type":"code","source":"SUBMISSION_PATH = '/kaggle/working'\nsubmission_id = 'submission'\nsubmission_path = os.path.join(SUBMISSION_PATH, '{}.csv'.format(submission_id))\nsample_submission = pd.DataFrame(submission, columns=[\"image_id\",\"PredictionString\"])\nsample_submission.to_csv(submission_path, index=False)\nsubmission_df = pd.read_csv(submission_path)\nsubmission_df","execution_count":null,"outputs":[]}],"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":4,"nbformat_minor":4}