{"cells":[{"metadata":{},"cell_type":"markdown","source":"# **Wheat Detection with FasterRCNN**\n","execution_count":null},{"metadata":{"_uuid":"8f2839f25d086af736a60e9eeb907d3b93b6e0e5","_cell_guid":"b1076dfc-b9ad-4769-8c92-a6c4dae69d19","trusted":true},"cell_type":"code","source":"import sys\nsys.path.insert(0,\"../input/weightedboxesfusion\")\nimport numpy as np \nimport pandas as pd \nimport 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 os\nimport glob\nfrom tqdm.auto import tqdm\nfrom tqdm import tqdm_notebook\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 albumentations import (Blur, MotionBlur, MedianBlur, GaussianBlur,\n                            VerticalFlip, HorizontalFlip, IAASharpen,\n                            OneOf, Compose , BboxParams, Resize ,RandomSizedCrop,\n                            ToGray , Cutout , HueSaturationValue , RandomBrightnessContrast)","execution_count":null,"outputs":[]},{"metadata":{},"cell_type":"markdown","source":"# **Path directories**","execution_count":null},{"metadata":{"_uuid":"d629ff2d2480ee46fbb7e2d37f6b5fab8052498a","_cell_guid":"79c7e3d0-c299-4dcb-8224-4455121ee9b0","trusted":true,"scrolled":true},"cell_type":"code","source":"DIR_PATH = '/kaggle/input/global-wheat-detection/'\ndir = glob.glob(os.path.join(DIR_PATH , '*'))\ndir.sort(reverse=True)\ntrain_paths = glob.glob(os.path.join(dir[1] , '*'))\ntest_paths = glob.glob(os.path.join(dir[2] , '*'))","execution_count":null,"outputs":[]},{"metadata":{},"cell_type":"markdown","source":"# **Read Dataframe**","execution_count":null},{"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.head()","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**\n\n**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":"**Cutout**","execution_count":null},{"metadata":{"trusted":true},"cell_type":"code","source":"images_after_augmentation(image, boxes, Cutout(num_holes=8, max_h_size=128, max_w_size=128, fill_value=0, p=1.0))","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 , paths , dataframe ,  transforms = None):\n        super().__init__()\n        self.paths = paths\n        self.dataframe = dataframe\n        self.transforms = transforms\n    def __getitem__(self, index):\n        path_image = self.paths[index]\n        image, boxes , area = self.load_image_and_boxes(index)\n       \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        target['area'] = area\n\n        if self.transforms:\n            sample = self.transforms(**{\n                  'image': image,\n                  'bboxes': target['boxes'],\n                  'labels': labels\n              })\n            image = sample[\"image\"]\n            target['bboxes'] = torch.as_tensor(sample['bboxes'], dtype=torch.float32)\n            target['bboxes'] =  target['bboxes'].reshape(-1, 4)\n            target[\"boxes\"] = target[\"bboxes\"]\n            del target['bboxes']\n        else:\n            target['boxes'] = torch.as_tensor(target['boxes'], dtype=torch.float32)\n            target['boxes'] =  target['boxes'].reshape(-1, 4)\n        image = np.transpose(image,(2,0,1))\n        image = torch.from_numpy(image)\n        return image, target, path_image\n    \n    def __len__(self):\n        return len(self.paths)\n\n    def load_image_and_boxes(self, index):\n        path_image = self.paths[index]\n        image_id = path_image.split('/')[-1].split('.')[0]\n        image = cv2.imread(path_image, 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']].to_numpy()\n        area = (boxes[:, 2] * boxes[:, 3])\n        boxes[:, 2] = boxes[:, 0] + boxes[:, 2]\n        boxes[:, 3] = boxes[:, 1] + boxes[:, 3]\n        area = torch.as_tensor(area, dtype=torch.float32)\n        return image, boxes , area\n    \n    def load_cutmix_image_and_boxes(self, index, imsize=1024):\n        \"\"\" \n        This implementation of cutmix author:  https://www.kaggle.com/nvnnghia \n        Refactoring and adaptation: https://www.kaggle.com/shonenkov\n        \"\"\"\n        w, h = imsize, imsize\n        s = imsize // 2\n    \n        xc, yc = [int(random.uniform(imsize * 0.25, imsize * 0.75)) for _ in range(2)]  # center x, y\n        indexes = [index] + [random.randint(0, len(self.paths) - 1) for _ in range(3)]\n\n        result_image = np.full((imsize, imsize, 3), 1, dtype=np.float32)\n        result_boxes = []\n\n        for i, index in enumerate(indexes):\n            image, boxes = self.load_image_and_boxes(index)\n            if i == 0:\n                x1a, y1a, x2a, y2a = max(xc - w, 0), max(yc - h, 0), xc, yc  # xmin, ymin, xmax, ymax (large image)\n                x1b, y1b, x2b, y2b = w - (x2a - x1a), h - (y2a - y1a), w, h  # xmin, ymin, xmax, ymax (small image)\n            elif i == 1:  # top right\n                x1a, y1a, x2a, y2a = xc, max(yc - h, 0), min(xc + w, s * 2), yc\n                x1b, y1b, x2b, y2b = 0, h - (y2a - y1a), min(w, x2a - x1a), h\n            elif i == 2:  # bottom left\n                x1a, y1a, x2a, y2a = max(xc - w, 0), yc, xc, min(s * 2, yc + h)\n                x1b, y1b, x2b, y2b = w - (x2a - x1a), 0, max(xc, w), min(y2a - y1a, h)\n            elif i == 3:  # bottom right\n                x1a, y1a, x2a, y2a = xc, yc, min(xc + w, s * 2), min(s * 2, yc + h)\n                x1b, y1b, x2b, y2b = 0, 0, min(w, x2a - x1a), min(y2a - y1a, h)\n            result_image[y1a:y2a, x1a:x2a] = image[y1b:y2b, x1b:x2b]\n            padw = x1a - x1b\n            padh = y1a - y1b\n\n            boxes[:, 0] += padw\n            boxes[:, 1] += padh\n            boxes[:, 2] += padw\n            boxes[:, 3] += padh\n\n            result_boxes.append(boxes)\n\n        result_boxes = np.concatenate(result_boxes, 0)\n        np.clip(result_boxes[:, 0:], 0, 2 * s, out=result_boxes[:, 0:])\n        result_boxes = result_boxes.astype(np.int32)\n        result_boxes = result_boxes[np.where((result_boxes[:,2]-result_boxes[:,0])*(result_boxes[:,3]-result_boxes[:,1]) > 0)]\n        return result_image, result_boxes\n","execution_count":null,"outputs":[]},{"metadata":{},"cell_type":"markdown","source":"**Split 80% of data to train and 20% to validation**","execution_count":null},{"metadata":{"trusted":true},"cell_type":"code","source":"train_path , valid_path = train_test_split(train_paths,test_size=0.2)","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                            Cutout(num_holes=8, max_h_size=128, max_w_size=128, fill_value=0, p=0.5),\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)]),\n                            OneOf([Blur(blur_limit=3), MotionBlur(blur_limit=3), MedianBlur(blur_limit=3)]),\n                            OneOf([VerticalFlip(), HorizontalFlip()])\n                            ],p = 0.9,bbox_params=BboxParams(format='pascal_voc', min_area=0, \n                                               min_visibility=0, label_fields=['labels']))","execution_count":null,"outputs":[]},{"metadata":{},"cell_type":"markdown","source":"# **Dataloader**","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 = 8\nnum_workers = 8\ntrain_set = WheatDataset(train_path,df , transforms=transforms_train)\nvalid_set = WheatDataset(valid_path,df)\n\ntrain_loader = DataLoader(train_set,batch_size = bs ,shuffle = True,collate_fn=collate_fn , num_workers=num_workers)\nvalid_loader = DataLoader(valid_set,batch_size = bs ,shuffle = True,collate_fn=collate_fn , num_workers=num_workers)\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        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(2)}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      for images, targets, image_ids in tqdm_notebook(train_loader):\n        batch_size = len(images)\n        images = list(image.to(device).float() for image in images)\n        target_res = [{k: v.to(device) for k, v in t.items()} for t in targets]\n        loss_dict = self.model(images, target_res)\n        losses = sum(loss for loss in loss_dict.values())\n        loss_value = losses.item()\n        self.optimizer.zero_grad()\n        losses.backward()\n        self.optimizer.step()\n        calc_loss.update(loss_value, batch_size)\n\n        if self.config.step_scheduler:\n          self.scheduler.step()\n      \n      return calc_loss\n\n\n    def valid_one_epoch(self, val_loader):\n        self.model.train()\n        calc_loss = Calculator()\n        t = time.time()\n        for images, targets, image_ids in tqdm_notebook(val_loader):\n            with torch.no_grad():\n                batch_size = len(images)\n                images = list(image.to(device).float() for image in images)\n                target_res = [{k: v.to(device) for k, v in t.items()} for t in targets]\n                loss_dict = self.model(images , target_res)\n                losses = sum(loss for loss in loss_dict.values())\n                loss_value = losses.item()\n                calc_loss.update(loss_value, 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":"# **FasterRCNN Model**\n> num_classes = 2 - class 1 for Wheat and 0 for background","execution_count":null},{"metadata":{"trusted":true},"cell_type":"code","source":"def get_model(num_classes = 2):\n  fpn_resnet = torchvision.models.detection.fasterrcnn_resnet50_fpn(pretrained=False , pretrained_backbone = False)\n  in_features = fpn_resnet.roi_heads.box_predictor.cls_score.in_features\n  fpn_resnet.roi_heads.box_predictor = FastRCNNPredictor(in_features, num_classes)\n  fpn_resnet._name_ = \"fpn_resnet\"\n  return fpn_resnet","execution_count":null,"outputs":[]},{"metadata":{"trusted":true},"cell_type":"code","source":"class GlobalParametersTrain:\n    lr = 0.0003\n    n_epochs = 40 \n\n    folder = '/kaggle/input/modelfasterrcnn'\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":{"trusted":true},"cell_type":"code","source":"device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')\ntraining = Training(model=get_model().to(device), device=device, config=GlobalParametersTrain)\n\nimages, targets, image_ids = next(iter(valid_loader))\nimages = list(img.to(device) for img in images)\ntargets = [{k: v.to(device) for k, v in t.items()} for t in targets]\nboxes = targets[0]['boxes'].cpu().numpy().astype(np.int32)\nsample = images[0].permute(1,2,0).cpu().numpy()\n\ntraining.load_model('../input/fasterrcnnmodel049640/best-model-36epoch.bin')\nmodel = training.model\nmodel.eval()\ncpu_device = torch.device(\"cpu\")\n\noutputs = model(images)\noutputs = [{k: v.to(cpu_device) for k, v in t.items()} for t in outputs]\n\nfig, ax = plt.subplots(1, 1, figsize=(16, 8))\n\nthreshold = 0.95\nfor i , score in enumerate(outputs[0]['scores']):\n  if score.item() > threshold:\n    for box in outputs[0]['boxes']:\n        cv2.rectangle(sample,(box[0], box[1]),(box[2] , box[3]),(255, 255, 0), 3)\n    \nax.set_axis_off()\nax.imshow(sample)","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,weights=None,image_size=1024,iou_thr=0.5,skip_box_thr=0.43):\n    boxes_list = [(pred[\"boxes\"] / (image_size-1)).tolist() for pred in predictions]\n    scores_list = [pred[\"scores\"].tolist() for pred in predictions]\n    labels_list = [np.ones(len(score)).astype(int).tolist() for score in scores_list]\n    boxes, scores, labels = weighted_boxes_fusion(boxes_list, scores_list, labels_list, weights=weights, iou_thr=iou_thr, skip_box_thr=skip_box_thr)\n\n    return boxes, scores, labels","execution_count":null,"outputs":[]},{"metadata":{},"cell_type":"markdown","source":"# **Load Best Model**","execution_count":null},{"metadata":{"trusted":true},"cell_type":"code","source":"training = Training(model=get_model(2).to(device), device=device, config=GlobalParametersTrain)\ntraining.load_model('../input/fasterrcnnmodel049640/best-model-36epoch.bin')\nmodel = training.model","execution_count":null,"outputs":[]},{"metadata":{},"cell_type":"markdown","source":"# **TTA Class**","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":{"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 display_tta(path):\n    iou_thr = 0.4\n    detection_threshold = 0.5\n    model.cuda()\n    model.eval()\n\n    img = plt.imread(path).astype(np.float32)\n    img /= 255.0\n    \n    t = Compose([Resize(width=512,height=512),ToTensorV2()])\n    data = { \"image\": img}\n    data = t(**data)\n    \n    img_tensor = data[\"image\"]\n    img_tensor = img_tensor.squeeze(0)\n    selected_tta = random.choice(tta_transforms)\n    \n    tta_image = selected_tta.augment(img_tensor)\n    outputs = model(tta_image.unsqueeze(0).cuda())\n    boxes = outputs[0]['boxes'].data.detach().cpu().numpy()\n    scores = outputs[0]['scores'].data.detach().cpu().numpy() \n    \n    boxes = boxes[scores >= detection_threshold]\n    scores = scores[scores >= detection_threshold]\n    original_boxes  = selected_tta.deaugment_boxes(boxes.copy())\n    \n    img_tta = tta_image.permute(1,2,0).detach().cpu().numpy()\n    img = img_tensor.permute(1,2,0).detach().cpu().numpy()\n\n    return img , img_tta , boxes , original_boxes","execution_count":null,"outputs":[]},{"metadata":{},"cell_type":"markdown","source":"# **Examples with TTA  - Test time augmentation**","execution_count":null},{"metadata":{"trusted":true},"cell_type":"code","source":"for image_path in test_paths:\n    img , tta_img , boxes , original_boxes = display_tta(image_path)\n    original_with_boxes = visualizeTarget(img,{\"boxes\": original_boxes},visualize_data_loader = False)\n    tta_with_boxes = visualizeTarget(tta_img,{\"boxes\": boxes},visualize_data_loader = False)\n    visualize(input_image = img, image_with_tta_bounding_boxes = tta_with_boxes , original_with_bounding_boxes = original_with_boxes)","execution_count":null,"outputs":[]},{"metadata":{"trusted":true},"cell_type":"code","source":"def format_prediction_string(boxes, scores):\n    pred_strings = []\n    for row in zip(scores, boxes):\n        score , x_min , y_min , x_max,y_max =row[0], row[1][0], row[1][1], row[1][2], row[1][3]\n        x = round(x_min)\n        y = round(y_min)\n        h = round(x_max-x_min)\n        w = round(y_max-y_min)\n        pred_strings.append(\"{0:.4f} {1} {2} {3} {4}\".format(score, x , y , h , w))\n    return \" \".join(pred_strings)","execution_count":null,"outputs":[]},{"metadata":{"trusted":true},"cell_type":"code","source":"model.cuda()\nmodel.eval()\n\niou_thr = 0.4\ndetection_threshold = 0.5\nn = 20\n\nsubmission = []\n\nfor image_path in tqdm(test_paths):\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    t = Compose([Resize(width=512,height=512),ToTensorV2()])\n    data = { \"image\": image}\n    data = t(**data)\n    image = data[\"image\"]\n    image = image.squeeze(0)\n   \n\n    predictions = []\n    \n    for i in range(n):\n        selected_tta = random.choice(tta_transforms)\n        tta_image = selected_tta.augment(image) ## need to be random\n        outputs = model(tta_image.unsqueeze(0).cuda())\n        boxes = outputs[0]['boxes'].data.detach().cpu().numpy()\n        scores = outputs[0]['scores'].data.detach().cpu().numpy()\n        boxes = boxes[scores >= detection_threshold]\n        scores = scores[scores >= detection_threshold]\n        original_boxes  = selected_tta.deaugment_boxes(boxes)\n        predictions.append({\"boxes\"  : original_boxes,'scores': scores})\n    \n    boxes, scores, labels = run_wbf(predictions,iou_thr=iou_thr,image_size=512)\n    boxes = boxes * 1024.0\n        \n    image = image.permute(1,2,0).detach().cpu().numpy()\n    image = cv2.resize(image,(1024,1024))\n    original_with_boxes = visualizeTarget(image,{\"boxes\": boxes} , visualize_data_loader = False)\n    \n    visualize(image_test=original_with_boxes)\n\n    prediction_string = format_prediction_string(boxes, scores)\n    \n    image_name = image_path.split(\"/\")[-1].split(\".\")[0]\n    submission.append([image_name, prediction_string])","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}