{"cells":[{"metadata":{"_uuid":"8f2839f25d086af736a60e9eeb907d3b93b6e0e5","_cell_guid":"b1076dfc-b9ad-4769-8c92-a6c4dae69d19","trusted":true},"cell_type":"markdown","source":"Some really important links this notebook works on:\n\n1. YOLO basics: https://www.analyticsvidhya.com/blog/2018/12/practical-guide-object-detection-yolo-framewor-python/\n2. Implementing albumentations with tf.keras's Sequence: https://medium.com/the-artificial-impostor/custom-image-augmentation-with-keras-70595b01aeac\n3. Majority of content was inspired from this awesome Kernel: https://www.kaggle.com/mattbast/object-detection-tensorflow-end-to-end\n\n## Import the neccessary modules:","execution_count":null},{"metadata":{"_uuid":"d629ff2d2480ee46fbb7e2d37f6b5fab8052498a","_cell_guid":"79c7e3d0-c299-4dcb-8224-4455121ee9b0","trusted":true},"cell_type":"code","source":"import numpy as np\nimport pandas as pd\nimport matplotlib.pyplot as plt\nimport tensorflow as tf\nimport albumentations as albu\nfrom tqdm import tqdm\nfrom PIL import Image, ImageDraw\n\nimport os\n\nplt.style.use(\"dark_background\")\n%matplotlib inline","execution_count":null,"outputs":[]},{"metadata":{},"cell_type":"markdown","source":"### Load the neccessary files","execution_count":null},{"metadata":{"trusted":true},"cell_type":"code","source":"! ls ../input/global-wheat-detection/","execution_count":null,"outputs":[]},{"metadata":{"trusted":true},"cell_type":"code","source":"main_dir = \"../input/global-wheat-detection/\"\n\ntrain_files = tf.io.gfile.glob(main_dir + \"train/*\")\ntest_files = tf.io.gfile.glob(main_dir + \"test/*\")\n\nprint (\"No of Train files: {}\\nNo of Test files: {}\".format(len(train_files), len(test_files)))","execution_count":null,"outputs":[]},{"metadata":{"trusted":true},"cell_type":"code","source":"sample_sub = pd.read_csv(main_dir + \"sample_submission.csv\")\ntrain = pd.read_csv(main_dir + \"train.csv\")\n\ntrain.shape, sample_sub.shape","execution_count":null,"outputs":[]},{"metadata":{},"cell_type":"markdown","source":"### EDA:","execution_count":null},{"metadata":{"trusted":true},"cell_type":"code","source":"# how does the sub file look like?\nsample_sub.head()","execution_count":null,"outputs":[]},{"metadata":{},"cell_type":"markdown","source":"It takes the following format: \n\n    img_id | confidence x y w h\n\nA short desc on possible bounding box label representations. Note that our data is stored and expected to be of the **Coco Format**:\n\n<img src=data:image/png;base64,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AjUsQFBYwyefQ0cAAQQQQAABBBBAAAEECAr5G0AAAQQQQAABBBBAAAEEaliAoLCGTz6HjgACCCCAAAIIIIAAAggQFPI3gAACCCCAAAIIIIAAAgjUsABBYQ2ffA4dAQQQQAABBBBAAAEEECAo5G8AAQQQQAABBBBAAAEEEKhhAYLCGj75HDoCCCCAAAIIIIAAAgggQFDI3wACCCCAAAIIIIAAAgggUMMCBIU1fPI5dAQQQAABBBBAAAEEEECAoJC/AQQQQAABBBBAAAEEEECghgUICmv45HPoCCCAAAIIIIAAAggggABBIX8DCCCAAAIIIIAAAggggEANCxAU1vDJ59ARQAABBBBAAAEEEEAAAYJC/gYQQAABBBBAAAEEEEAAgRoWICis4ZPPoSOAAAIIIIAAAggggAACBIX8DSCAAAIIIIAAAggggAACNSxAUFjDJ59DRwABBBBAAAEEEEAAAQQICvkbQAABBBBAAAEEEEAAAQRqWICgsIZPPoeOAAIIIIAAAggggAACCBAU8jeAAAIIIIAAAggggAACCNSwAEFhDZ98Dh0BBBBAAAEEEEAAAQQQICjkbwABBBBAAAEEEEAAAQQQqGEBgsIaPvkcOgIIIIAAAggggAACCCBAUMjfAAIIIIAAAggggAACCCBQwwIEhTV88jl0BBBAAIHqEgiCQP766y/RTxICCCDQmgL//POP/P33361ZBPZdhABBYRFYrIoAAggggEAlC9x8880y99xzy+OPP17JxaRsCCBQAwILLbSQbLrppjVwpNVxiASF1XEeOQoEEEAAAQSkrq4OBQQQQKAiBH755ZeKKAeFKEyAoLAwJ9ZCAAEEEEAAAQQQQAABBKpSYI6qPCoOqlUFxo8fL88995y899570qlTJ9l5551lscUWi8qkbcyHDh0q48aNE72L1LFjR9l///1lmWWWidbRiT///NPn8+yzz8oPP/wgu+66q2y77bYy33zzResVmle0ARMIIFBxAr///rv0799fFl10UTn88MMzyvfxxx/LiBEjZIsttvA/uvDDDz+UYcOGyUcffSRLLLGEbLbZZrLXXnuJMSZj208//VSefvppefnll309o3XI6quvnrFO9pfvvvtObrvtNunSpYt8+eWX8sQTT/hVdtppJ1//tGnTJmOTF154QV588UVfpnnmmUdWWWUVOfjgg/2xhCt+/fXXcvfdd8s777wjc845p6y55pq+ztOyp6dC6s6HHnpI3n77bX/s8847r2+addhhh8lcc82VnhXTCCCAQNEC99xzj3z++edy5JFHysILLxxtb62Va665RhZccMGojtbrssGDB/t6Teuf9dZbz9d9Wi81lJq6XUN5sqxMAu5EkxAom8D1119v3Z+m/1l66aWjaXfh5Pfx448/2rXXXjua7y4C/fT8889vH3300agcLtizXbt2jdbT5Zqvfn7//fdF5RVlygQCCFSswPrrr+//jc+YMSOjjCeddJKf/+STT/r5t99+e1QvpNcx7uaT/eOPP6Jtx44dm3O9Sy+9NFon18QHH3zgtwvz1s+wntp9992ta54ZbXbcccdF+wjXCeupL774wq/37bff2hVXXNGvp/VXel3mgsQor8bqThc429BI95G+PxcwW9exjM/LBdd+X+n1abQTJhBAAIEGBC6++GJffwwYMCBjrbA+7d27t5//+uuvR3WZ1kVhvaZ1nbtZF22rdZXWW2EqdLtwfT5bVkBadnfsrZoFRo8e7SuTjTbayIYXdq+88oqfp5WCXky5pwD+u1Ysroc8z+Hu5Pt5Wqm4u/R+3nnnnefnHX300dY9MbQaJIYXO6eeeqpfp9C8qtmcY0OgWgT0IkQvIPTfeZi0jtB6QS86XA929v333/fr6Pc33njDr/brr79GN5D0gkaTe8Lnt9NtX331VT9P66RVV13Vb+/uhPt5uX6FQaGW5eqrr/ar6L7//e9/+22vuOIKP08vfHQdDRrD+u6rr76KyqJBnqZbb73Vr3fZZZf5ekzrMnfH3c879thj/TqF1J3ujrzfplu3blaPWZOWNQw4Q4+wniQo9ET8QgCBIgQmT57s6xm9jktP4Q0w15rBzpw5M6p3XAsIv5relOrbt6/fdrvttotuUqUHhcVsl75vpltOgKCw5ayrfk8nn3yyrxD0jlJ60ouUM88803722Wd+uV5EaaCXni644AK/TJ8CaNKniXpB55qXRqvpxdSJJ55oL7/8cr99eEHWWF5RBkwggEDFCugTtfQLCC3oqFGj/Lxzzz3Xl1sDK13nlltuyTgObT2g8zVY1PTII4/471pXpKennnrK1yF6tzpfCoNCvZEVPn3TdbWVg+7D9aTnN500aZLVANE1lc/I6uGHH/br6Y0tTWGwe9RRR9lvvvnGz9Mg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CCCAAAIIIIAAAnEUsC5wC049R0QDw7ZtJeh5ksjkqU06FNN1L7+d9kRqH3vST5sD9m1SXmzUPAIEhc3jSq4IIIAAAggggAACCMRT4M+/xB4x6z3CW66VxA39/HEEPXrP1otoQQe49JJiumzrA0J7210iK68oZoN1C9qUlVpGgKCwZZzZCwIIIIAAAggggAACsRAI+lwpduL7Yg7tJma7TmL23s3/2FffEHtVqgMaqauTunZL+59CDsocvH9qtW++lYT2SEqqKAGCwoo6HRQGAQQQQAABBBBAAIHWE4gCP9dsNHHJuVFBEldclGpGevEVYt95zw9uHy0sYMLstL3fXlc1/5dqTlrAZqzSQgJztNB+2A0CCCCAAAIIIIAAAghUuIDZaH1J/vzZ7KVcdBFJfvFBxvzE4w+4dw1PzJiX98vMmb4XU9/j6JKL512NBa0jQFDYOu7sFQEEEEAAAQQQQACB+Ar8MVPsf88Xs4MbgL6h5NYTayXoe61fyxyR6om0oU1Y1vICBIUtb84eEUAAAQQQQAABBBCIt4AbTsL8p6eYPXdp8DiCcy4Re/Mgv47ptIULIrdrcH0Wto4AQWHruLNXBBBAAAEEEEAAAQRiLWD23aPR8psu24hMnSayTkdJHHe0SDLZ6Das0PICBIUtb84eEUAAAQQQQAABBBCoCQF9h9C/R1gTRxvfg6T30fieO0qOAAIIIIAAAggggAACCJQsQFBYMiEZIIAAAggggAACCCCAAALxFSAojO+5o+QIIIAAAggggAACCCCAQMkCBIUlE5IBAggggAACCCCAAAIIIBBfAYLC+J47So4AAggggAACCCCAAAIIlCxAUFgyIRkggAACCCCAAAIIIIAAAvEVICiM77mj5AgggAACCCCAAAIIIIBAyQIEhSUTkgECCCCAAAIIIIAAAgggEF8BgsL4njtKjgACCCCAAAIIIIAAAgiULEBQWDIhGSCAAAIIIIAAAggggAAC8RUgKIzvuaPkCCCAAAIIIIAAAggggEDJAgSFJROSAQIIIIAAAggggAACCCAQXwGCwvieO0qOAAIIIIAAAggggAACCJQsQFBYMiEZIIAAAggggAACCCCAAALxFSAojO+5o+QIIIAAAggggAACCCCAQMkCBIUlE5IBAggggAACCCCAAAIIIBBfAYLC+J47So4AAggggAACCCCAAAII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>","execution_count":null},{"metadata":{"trusted":true},"cell_type":"code","source":"train.head()","execution_count":null,"outputs":[]},{"metadata":{"trusted":true},"cell_type":"code","source":"# how many unique values does each col contain?\nprint (\"Tot rows:\", train.shape[0])\ntrain.nunique()","execution_count":null,"outputs":[]},{"metadata":{"trusted":true},"cell_type":"code","source":"train.width.unique(), train.height.unique()","execution_count":null,"outputs":[]},{"metadata":{},"cell_type":"markdown","source":"Height and width have the same value and are redundant. Let's drop them.","execution_count":null},{"metadata":{"trusted":true},"cell_type":"code","source":"train.drop(['width', 'height'], axis=1, inplace=True)","execution_count":null,"outputs":[]},{"metadata":{"trusted":true},"cell_type":"code","source":"temp = train.source.value_counts()\nax = temp.plot(kind='bar', figsize=(15, 5), title='Source: Count plots')\nfor i, value in enumerate(temp):\n    ax.text(i-0.1, value, value);","execution_count":null,"outputs":[]},{"metadata":{},"cell_type":"markdown","source":"'''\nYou will use more than 3,000 images from Europe (France, UK, Switzerland) and North America (Canada). The test data includes about 1,000 images from Australia, Japan, and China.\n'''","execution_count":null},{"metadata":{"trusted":true},"cell_type":"code","source":"# An image can obviously come only from one source\n(train.groupby(\"image_id\")['source'].nunique() != 1).any()","execution_count":null,"outputs":[]},{"metadata":{"trusted":true},"cell_type":"code","source":"train.info()","execution_count":null,"outputs":[]},{"metadata":{"trusted":true},"cell_type":"code","source":"# bbox is stored as strings, let process them to list values\ntrain['bbox'] = train['bbox'].transform(lambda x: list(map(float, x.strip(\"[]\").split(\", \"))))","execution_count":null,"outputs":[]},{"metadata":{"trusted":true},"cell_type":"code","source":"labels = train.groupby(\"image_id\")['bbox'].apply(np.stack)\nlen(labels)","execution_count":null,"outputs":[]},{"metadata":{"trusted":true},"cell_type":"code","source":"# how many bbox on avg per image?\ntemp = labels.apply(len)\ntemp.describe()","execution_count":null,"outputs":[]},{"metadata":{"trusted":true},"cell_type":"code","source":"train.head()","execution_count":null,"outputs":[]},{"metadata":{"trusted":true},"cell_type":"code","source":"# avg bounding box based on source\ntemp = train.groupby([\"source\", \"image_id\"]).count().reset_index()\ntemp.groupby(\"source\")[\"bbox\"].agg([\"count\", \"min\", \"mean\", \"max\", \"std\"])","execution_count":null,"outputs":[]},{"metadata":{"trusted":true},"cell_type":"code","source":"f, ax = plt.subplots(nrows=7, figsize=(20, 15))\nfor i, source in enumerate(sorted(train.source.unique())):\n    temp.query(f\"source == '{source}'\")['bbox'].plot(kind='hist', ax=ax[i], title=source)\n    \nf.suptitle(\"Visualizing BBox Distributions for various sources\")\nf.tight_layout(rect=[0, 0.03, 1, 0.95]);","execution_count":null,"outputs":[]},{"metadata":{},"cell_type":"markdown","source":"### Let's plot Em images!\nLets write a simple utility function that when given an image_id & bbox, returns an image with bboxes drawn over it: ","execution_count":null},{"metadata":{"trusted":true},"cell_type":"code","source":"def draw_bboxes(img_inp, bboxes, source='train', W=8, img_id_as_input=True, color='red'):\n    '''Curtesy of : Mattbast\n    From: https://www.kaggle.com/mattbast/object-detection-tensorflow-end-to-end'''\n    \n    def draw_bbox(draw, bbox):\n        x, y, width, height = bbox\n        draw.rectangle([x, y, x+width, y+height], width=W, outline=color)\n        \n    if img_id_as_input:\n        image = Image.open(f\"{main_dir}{source}/{img_inp}.jpg\")\n    else:\n        image = Image.fromarray(img_inp)\n        \n    draw = ImageDraw.Draw(image)\n    \n    for bbox in bboxes:\n        draw_bbox(draw, bbox)\n        \n    return np.array(image)","execution_count":null,"outputs":[]},{"metadata":{},"cell_type":"markdown","source":"Plotting those images with very little bboxes and those with a lot of bboxes:","execution_count":null},{"metadata":{"trusted":true},"cell_type":"code","source":"f, ax = plt.subplots(ncols=5, nrows=3, figsize=(20, 10))\nax = ax.ravel()\n\nfor i, (_, img_id, count) in enumerate(temp[[\"image_id\", \"bbox\"]][temp.bbox < 3].sort_values(by='bbox').itertuples()):\n    ax[i].imshow(draw_bboxes(img_id, labels[img_id]))\n    ax[i].set_title(\"Bbox count: \" + str(count))\n    ax[i].axis('off')  \n    \nf.suptitle(\"Tiny No. of bboxes!\");\n\nf, ax = plt.subplots(ncols=4, nrows=2, figsize=(20, 10))\nfor i, (_, img_id, count) in enumerate(temp[[\"image_id\", \"bbox\"]][temp.bbox > 100].sort_values(by='bbox').itertuples()):\n    \n    ax[0][i].imshow(plt.imread(f\"{main_dir}train/{img_id}.jpg\"))\n    ax[0][i].axis('off')\n    ax[0][i].set_title(f\"{img_id} without BBox\")\n    \n    ax[1][i].imshow(draw_bboxes(img_id, labels[img_id]))\n    ax[1][i].set_title(\"Bbox Count: \" + str(count))\n    ax[1][i].axis('off')  \n    \nf.suptitle(\"Large No. of bboxes!\");","execution_count":null,"outputs":[]},{"metadata":{},"cell_type":"markdown","source":"Plot the images not found in CSV. Does any exist?","execution_count":null},{"metadata":{"trusted":true},"cell_type":"code","source":"# those images in csv but not actually present. No such image exits\nlen(np.setdiff1d(labels.index.values, list(map(lambda x: x.split(\"/\")[-1].split(\".\")[0], train_files))))","execution_count":null,"outputs":[]},{"metadata":{"trusted":true},"cell_type":"code","source":"# those images not in train.csv but in train images\nmissing_ids = np.setdiff1d(list(map(lambda x: x.split(\"/\")[-1].split(\".\")[0], train_files)), labels.index.values)\n\nf, ax = plt.subplots(ncols=5, nrows=5, figsize=(20, 20))\nax = ax.ravel()\nfor i, img_id in enumerate(np.random.choice(missing_ids, (25,))):\n    ax[i].imshow(plt.imread(f\"{main_dir}train/{img_id}.jpg\"))\n    ax[i].set_title(img_id)\n    ax[i].axis('off')","execution_count":null,"outputs":[]},{"metadata":{},"cell_type":"markdown","source":"#### Plotting the images from different categories:","execution_count":null},{"metadata":{"trusted":true},"cell_type":"code","source":"# plotting without bboxes\nf, ax = plt.subplots(nrows=5, ncols=7, figsize=(20, 15))\nfor i, (source, img_ids) in enumerate(train.groupby(\"source\")['image_id'].apply(lambda x: list(x.sample(5))).iteritems()):\n    for j, img_id in enumerate(img_ids):\n        ax[j][i].imshow(plt.imread(f\"{main_dir}train/{img_id}.jpg\"))\n        ax[j][i].set_title(source)\n        ax[j][i].axis('off')","execution_count":null,"outputs":[]},{"metadata":{"trusted":true},"cell_type":"code","source":"# with and Without Bounding boxes\nf, ax = plt.subplots(nrows=2, ncols=7, figsize=(20, 8))\ntemp = train.groupby(\"source\")['image_id'].apply(lambda x: x.sample(1)).reset_index(level=0)\n\nfor i, (_, source, img_id) in enumerate(temp.itertuples()):\n    ax[0][i].imshow(plt.imread(f\"{main_dir}train/{img_id}.jpg\"))\n    ax[0][i].set(title=source +\" Without Bboxes\")\n    ax[0][i].axis('off')\n    \n    ax[1][i].imshow(draw_bboxes(img_id, labels[img_id]))\n    ax[1][i].set(title=source +\" With Bboxes\")\n    ax[1][i].axis('off')\n    \nf.tight_layout();","execution_count":null,"outputs":[]},{"metadata":{},"cell_type":"markdown","source":"#### Plotting the test images:","execution_count":null},{"metadata":{"trusted":true},"cell_type":"code","source":"# how does the test images look like?\nf, ax = plt.subplots(nrows=2, ncols=5, figsize=(20, 8))\nax = ax.ravel()\n\nfor i, img_loc in enumerate(test_files):\n    ax[i].imshow(plt.imread(img_loc))\n    ax[i].axis('off')\n    ax[i].set_title(img_loc.split(\"/\")[-1].split(\".\")[0])","execution_count":null,"outputs":[]},{"metadata":{},"cell_type":"markdown","source":"Let's remove those bounding boxes which have very small height or width:","execution_count":null},{"metadata":{"trusted":true},"cell_type":"code","source":"# How many such exists?\nremoved_bboxes = []\ncleaned_labels = {}\n\nfor img_id, bboxes in labels.iteritems():\n    for bbox in bboxes:\n        if bbox[2] < 10 or bbox[3] < 10:\n            removed_bboxes.append((img_id, bbox))\n        else:\n            cleaned_labels[img_id] = cleaned_labels.get(img_id, []) + [bbox]\n            \nlabels = pd.Series(map(np.stack, cleaned_labels.values()), index=cleaned_labels.keys()).rename_axis(\"image_id\")\ndel cleaned_labels\n\n\"No of bboxes removed: {}\".format(len(removed_bboxes))","execution_count":null,"outputs":[]},{"metadata":{},"cell_type":"markdown","source":"Do a simple Train / Val split:","execution_count":null},{"metadata":{"trusted":true},"cell_type":"code","source":"train_img_ids = labels.iloc[:int(0.95 * len(labels))].index\nval_img_ids = np.setdiff1d(labels.index, train_img_ids)\n\nlen(train_img_ids), len(val_img_ids)","execution_count":null,"outputs":[]},{"metadata":{},"cell_type":"markdown","source":"How does the image look like with 32 grids?","execution_count":null},{"metadata":{"trusted":true},"cell_type":"code","source":"GRID_SIZE = 32\n\nplt.figure(figsize=(15, 15))\ntemp = labels.map(len).argmax()\nplt.imshow(draw_bboxes(labels.index[temp], labels.iloc[temp]))\n\nbboxes = labels.iloc[temp].copy()\nbboxes[:, 0] = bboxes[:, 0] + (bboxes[:, 2] / 2)\nbboxes[:, 1] = bboxes[:, 1] + (bboxes[:, 3] / 2)\n\nplt.scatter(*list(zip(*bboxes[:, :2])), c='b')\n\nplt.vlines(range(0, 1024, 1024//GRID_SIZE), 0, 1024, color='y')\nplt.hlines(range(0, 1024, 1024//GRID_SIZE), 0, 1024, color='y')\nplt.title(\"Image with Max Bounding boxes\")\nplt.axis('off');","execution_count":null,"outputs":[]},{"metadata":{},"cell_type":"markdown","source":"The image looks pretty clustered. 5 anchors will do us good with grid_size of 32!","execution_count":null},{"metadata":{"trusted":true},"cell_type":"code","source":"IMG_SHAPE = (256, 256)\nFACTOR = 1024 / IMG_SHAPE[0]\nGRID_SIZE = 32\nANCHORS = 5\nBATCH_SIZE = 16","execution_count":null,"outputs":[]},{"metadata":{},"cell_type":"markdown","source":"Lets perform some data augmentations to our dataset using the albumentation library:\n\nAgain from <a href=https://www.kaggle.com/mattbast/object-detection-tensorflow-end-to-end>this</a> Notebook: \n\n''' \n- random sized crop: The model needs to be able to detect a wheat head regardless of how close or far away the head is to the camera. To produce more zoom levels in the dataset the crop method will take a portion of the image and zoom in to create a new image with larger wheat heads.\n\n- flip amd rotate: The wheat heads can point in any direction. To create more examples of wheat heads pointing in different directions the image will randomly be flipped both horizontally and vertically or rotated.\n\n- hue saturation and brightness: these are various methods that will alter the lighting of the image which will help to create different lighting scenarios. This helps as the test pictures are from various countries each with their own lighting levels.\n\n- noise: Some wheat heads aren't quite in focus. Adding some noise to the images helps to catch these wheat heads while also forcing the model to learn more abstract wheat head shapes. This helps a lot with over-fitting.\n\n- cutout: randomly remove small squares of pixels in the image. This prevents the model simply memorizing certain wheat heads and instead forces it to learn the patterns that represent a wheat head.\n\n- clahe: this is a must have. In many images the wheat heads are a similar colour to the grass in the background making it tricky for the model to differentiate between them. CLAHE helps to exemplify the colour difference between the two.\n\n- grayscale: I found that there were a few images with a yellow/gold tint. My model was learning to detect wheat heads without a tint (as most images do not contain a tint) and was really struggling to detect anything on the yellow images. By converting all images to grey scale the model is forced to ignore these tints making it much more effective at identifying wheat heads regardless of tint.\n\n'''\n\nLet's create augmentation Pipeline for processing our images:","execution_count":null},{"metadata":{"trusted":true},"cell_type":"code","source":"TRAIN_AUG = albu.Compose([\n    albu.RandomSizedCrop(\n            min_max_height=[int(IMG_SHAPE[0] * 0.85)] * 2, \n            height=IMG_SHAPE[0], \n            width=IMG_SHAPE[1], \n            p=0.8\n        ),\n    \n        albu.OneOf([\n            albu.Flip(),\n            albu.RandomRotate90(),\n        ], p=1),\n    \n        albu.OneOf([\n            albu.HueSaturationValue(),\n            albu.RandomBrightnessContrast()\n        ], p=1),\n    \n        albu.OneOf([\n                albu.GaussNoise(),\n                albu.GlassBlur(),\n                albu.ISONoise(),\n                albu.MultiplicativeNoise(),\n            ], p=0.5),\n    \n        albu.Cutout(\n            num_holes=8, \n            max_h_size=8, \n            max_w_size=8, \n            fill_value=0, \n            p=0.5\n        ),\n    \n        albu.CLAHE(p=1),\n        albu.ToGray(p=1),\n    ], \n        bbox_params={'format': 'coco', 'label_fields': ['labels']}\n)\n\nVAL_AUG = albu.Compose([\n    albu.CLAHE(p=1),\n    albu.ToGray(p=1)\n])","execution_count":null,"outputs":[]},{"metadata":{},"cell_type":"markdown","source":"Demo augmentation:","execution_count":null},{"metadata":{"trusted":true},"cell_type":"code","source":"org = plt.imread(f\"{main_dir}train/{img_id}.jpg\")\norg = tf.cast(tf.image.resize(org, IMG_SHAPE), tf.uint8).numpy()\norg_bboxes = labels[img_id] / FACTOR\naug = TRAIN_AUG(image=org, bboxes=org_bboxes, labels=np.ones(len(org_bboxes)))\n\naug.keys()","execution_count":null,"outputs":[]},{"metadata":{},"cell_type":"markdown","source":"Let's visualize how are augmentation pipeline works:","execution_count":null},{"metadata":{"trusted":true},"cell_type":"code","source":"f, ax = plt.subplots(figsize=(20, 20), ncols=3, nrows=3)\nax = ax.ravel()\n\nax[0].imshow(draw_bboxes(org, org_bboxes, img_id_as_input=False, W=2))\nax[0].set_title(\"Original Image\")\nax[0].axis('off')\n\nfor i in range(8):\n    aug = TRAIN_AUG(image=org, bboxes=org_bboxes, labels=np.ones(len(org_bboxes)))\n    ax[i + 1].imshow(draw_bboxes(aug['image'], aug['bboxes'], img_id_as_input=False, W=2))\n    ax[i + 1].axis(\"off\")\n    ax[i + 1].set_title(f\"Train Aug {i + 2}\")","execution_count":null,"outputs":[]},{"metadata":{"trusted":true},"cell_type":"code","source":"aug = VAL_AUG(image=org, bboxes=org_bboxes, labels=np.ones(len(org_bboxes)))\n\nf, ax, = plt.subplots(ncols=2, figsize=(16, 8))\nax[0].imshow(draw_bboxes(org, org_bboxes, img_id_as_input=False, W=2))\nax[0].set_title(\"Original Image\")\nax[0].axis('off')\nax[1].imshow(draw_bboxes(aug['image'], aug['bboxes'], img_id_as_input=False, W=2))\nax[1].axis(\"off\")\nax[1].set_title(\"Val Augmented\");","execution_count":null,"outputs":[]},{"metadata":{"trusted":true},"cell_type":"markdown","source":"Now that we have our augmentation pipeline ready, let's get on with creating a custom sequence generator using: `keras.utils.Sequence`. Before doing this however let's load all the images to memory for faster processing:","execution_count":null},{"metadata":{"trusted":true},"cell_type":"code","source":"def load_resize_image(image_loc):\n    image = tf.io.read_file(image_loc)\n    image = tf.image.resize(tf.image.decode_jpeg(image), IMG_SHAPE)\n    image = tf.cast(image, tf.uint8).numpy()\n    return image_loc[:-4].split(\"/\")[-1], image","execution_count":null,"outputs":[]},{"metadata":{"trusted":true},"cell_type":"code","source":"# load the images and resize them\ntrain_files = dict(map(lambda x: load_resize_image(x), tqdm(train_files)))","execution_count":null,"outputs":[]},{"metadata":{"trusted":true},"cell_type":"code","source":"# rain check! Working good?\ntemp = np.random.choice(train_img_ids)\nplt.figure(figsize=(10, 10))\nplt.imshow((draw_bboxes(train_files[temp], labels[temp]/FACTOR, W=2, img_id_as_input=False)))\nplt.axis('off');","execution_count":null,"outputs":[]},{"metadata":{},"cell_type":"markdown","source":"Let's create our Dataset generator:","execution_count":null},{"metadata":{"trusted":true},"cell_type":"code","source":"class GWD_Dataset(tf.keras.utils.Sequence):\n    def __init__(self, image_ids, image_pixels, labels=None, grid_size=GRID_SIZE, batch_size=1, shuffle=False, augment=False):\n        \n        self.image_ids = image_ids\n        self.image_pixels = image_pixels\n        self.labels = labels\n        self.grid_size = grid_size\n        self.batch_size = batch_size\n        self.shuffle = shuffle\n        self.augment = augment\n        self.on_epoch_end()\n        \n        self.train_augmentations = TRAIN_AUG\n        self.val_augmentations = VAL_AUG\n        \n        self.image_grid = self.form_image_grid()\n        \n        \n    def form_image_grid(self):   \n        '''We use this image grid later on for the creation of BBoxes which needs\n        to have relative x,y,h,w coordinates. And for rescaling back from the predictions.\n        \n        ** We use a 16 x 16 grid (self.grid_size) for our model. **\n        '''\n        \n        # Note the shape: [x, y, width, height] -> we create it in Coco format, later to YOLO\n        image_grid = np.zeros((self.grid_size, self.grid_size, 4))\n\n        # width=32, height=32 when grid_size=16 (size of each grid)\n        hw_grid = IMG_SHAPE[0] / self.grid_size\n        image_grid[..., 2:] = hw_grid\n\n        # increase width as we move towards the right\n        temp = np.arange(0, IMG_SHAPE[0], hw_grid)\n        image_grid[..., :1] =  temp.reshape(-1, 1)\n        \n        # increase height as we move down the grid (repeat n times before increasing)\n        image_grid[..., 1:2] = np.repeat(temp, self.grid_size).reshape(self.grid_size, self.grid_size, 1)\n\n        return image_grid\n    \n    def __len__(self):\n        '''Overriding the function to obtain train/val dataset length needed for fit(). \n        Returns number of batches in the Sequence.'''\n        \n        return len(self.image_ids) // self.batch_size\n\n\n    def on_epoch_end(self):\n        '''Executed automatically every end of each epoch.'''\n        \n        self.indexes = np.arange(len(self.image_ids))\n\n        if self.shuffle == True:\n            np.random.shuffle(self.indexes) # shuffle inplace\n            \n    \n    def __getitem__(self, index):\n        '''Gets batch AT a position index.'''\n        \n        indexes = self.indexes[index * self.batch_size:(index + 1) * self.batch_size]\n        batch_ids = [self.image_ids[i] for i in indexes]\n\n        X, y = self.__data_generation(batch_ids)\n\n        return X, y\n\n\n    def __data_generation(self, batch_ids):\n        X, y = [], []\n\n        # Generate data\n        for image_id in batch_ids:\n            pixels = self.image_pixels[image_id]\n            bboxes = self.labels[image_id]\n\n            if self.augment:     \n                pixels, bboxes = self.augment_image(pixels, bboxes)\n                \n            else:\n                pixels = self.contrast_image(pixels)\n                bboxes = self.form_label_grid(bboxes)\n\n            X.append(pixels)\n            y.append(bboxes)\n\n        return np.array(X), np.array(y)\n\n    def augment_image(self, pixels, bboxes):\n\n        aug_result = self.train_augmentations(image=pixels, bboxes=bboxes, labels=np.ones(len(bboxes)))\n        bboxes = self.form_label_grid(aug_result['bboxes'])\n\n        return np.array(aug_result['image']) / 255, bboxes\n\n    def contrast_image(self, pixels): \n        \n        aug_result = self.val_augmentations(image=pixels)\n        return np.array(aug_result['image']) / 255\n    \n    def form_label_grid(self, Bboxes):\n        'Function only depends on the bboxes and it can be any image'\n\n        label_grid = np.zeros((self.grid_size, self.grid_size, ANCHORS*5))\n\n        if len(Bboxes): # if no bounding boxes, return 0\n            # we convert all bboxes to yolo and return their centers \n            # which can then be used to create the label grid\n            bboxes, bx_centers, by_centers = self.yolo_shape(np.stack(Bboxes))\n\n        else:\n            return label_grid\n\n        for i in range(0, self.grid_size):\n            for j in range(0, self.grid_size):\n\n                # intersect those bboxes whoes centers lie \n                # inside cell we create n anchors\n                label_grid[i,j] = self.rect_intersect(self.image_grid[i,j], bboxes, bx_centers, by_centers)\n\n        return label_grid\n\n    def rect_intersect(self, cell, bboxes, bbox_x_centers, bbox_y_centers): \n\n        # if bbox lies with cell, True for that index\n        cond = (\n            (cell[0] <= bbox_x_centers) & \n            (cell[1] <= bbox_y_centers) & \n            (bbox_x_centers < cell[0] + cell[2]) & \n            (bbox_y_centers < cell[1] + cell[3])\n        )\n\n        # pad with 10 (2 anchor boxes)\n        temp_bboxes = bboxes[cond].ravel()\n        temp_bboxes = np.pad(temp_bboxes, [0, 5*ANCHORS])[:5*ANCHORS]\n\n        # output shape: (10,)\n        return temp_bboxes      \n\n    def yolo_shape(self, bboxes):\n        'Convert the bboxes to yolo format and scale them'\n\n        # scale factor\n        scale_factor = (IMG_SHAPE[0] // self.grid_size)\n\n        # calculate the box centers\n        bboxes[:, 0] =  bboxes[:, 0] + (bboxes[:, 2] / 2)\n        bboxes[:, 1] = bboxes[:, 1] + (bboxes[:, 3] / 2)\n\n        bbox_x_centers = bboxes[:, 0].copy()\n        bbox_y_centers = bboxes[:, 1].copy()\n\n        # we find the box lower limit given the grid size\n        # for eg: 13 lies between (0, 16); (8, 16); (12, 16)\n        # for grid sizes of 16, 32, 64..\n        x_lowers = (bbox_x_centers // scale_factor) * scale_factor\n        y_lowers = (bbox_y_centers // scale_factor) * scale_factor\n\n        # Offset bbox x,y to cell x,y\n        # bbox x,y,h,w will be calculated relative \n        # to the grid position they would belong to\n        bboxes[:, 0] = (bboxes[:, 0] - x_lowers) / scale_factor\n        bboxes[:, 1] = (bboxes[:, 1] - y_lowers) / scale_factor\n        bboxes[:, 2] = bboxes[:, 2] / IMG_SHAPE[0]\n        bboxes[:, 3] = bboxes[:, 3] / IMG_SHAPE[1]\n\n        return np.hstack([np.ones((len(bboxes), 1)), bboxes]), bbox_x_centers, bbox_y_centers","execution_count":null,"outputs":[]},{"metadata":{"trusted":true},"cell_type":"code","source":"strategy = tf.distribute.MirroredStrategy()","execution_count":null,"outputs":[]},{"metadata":{"trusted":true},"cell_type":"code","source":"with strategy.scope():\n\n    train_generator = GWD_Dataset(\n        train_img_ids,\n        train_files,\n        labels/FACTOR, \n        batch_size=BATCH_SIZE, \n        shuffle=True,\n        augment=True,\n    )\n\n    val_generator = GWD_Dataset(\n        val_img_ids,\n        train_files,\n        labels/FACTOR, \n        batch_size=BATCH_SIZE,\n        shuffle=False,\n        augment=False\n    )\n\nimage_grid = train_generator.image_grid\n\nlen(train_generator), len(val_generator)","execution_count":null,"outputs":[]},{"metadata":{"trusted":true},"cell_type":"code","source":"%%time\ntemp = train_generator[0]\ntemp[0].shape, temp[1].shape","execution_count":null,"outputs":[]},{"metadata":{"trusted":true},"cell_type":"code","source":"%%time\ntemp = val_generator[1]\ntemp[0].shape, temp[1].shape","execution_count":null,"outputs":[]},{"metadata":{},"cell_type":"markdown","source":"The input shape: (256, 256, 3) \nOutput shape: (32, 32, 10)\n\n<u>The basics we need to be aware of:</u>\n\nIn YOLO, the coordinates assigned to all the grids are:\n\n![](https://cdn.analyticsvidhya.com/wp-content/uploads/2018/12/Screenshot-from-2018-11-15-19-35-31.png)\n\n\nx,y are the coordinates of midpoint of object with respect to the grid\n- height is the ratio of height of bounding box WRT height of grid cell\n- width is the ratio of width of bounding box WRT width of grid cell\n\n`Note:` x and y will always range between 0 and 1 as the midpoint will always lie within the grid. Whereas height and width can be more than 1 in case the dimensions of the bounding box are more than the dimension of the grid.\n\nThe Non-Max Suppression technique cleans up this up so that we get only a single detection per object. Steps are as follows:\n\n0. Discard all the boxes having probabilities less than or equal to a pre-defined threshold (say, 0.5)\n\n1. It first looks at the probabilities associated with each detection and takes the largest one. In the above image, 0.9 is the highest probability, so the box with 0.9 probability will be selected first:\n![](https://cdn.analyticsvidhya.com/wp-content/uploads/2018/12/Screenshot-from-2018-11-17-12-08-14.png)\n\n2. Now, it looks at all the other boxes in the image. The boxes which have high IoU with the current box are suppressed. So, the boxes with 0.6 and 0.7 probabilities will be suppressed in our example:\n![](https://cdn.analyticsvidhya.com/wp-content/uploads/2018/12/Screenshot-from-2018-11-17-12-09-17.png)\n\n3. After the boxes have been suppressed, it selects the next box from all the boxes with the highest probability, which is 0.8 in our case:\n![](https://cdn.analyticsvidhya.com/wp-content/uploads/2018/12/Screenshot-from-2018-11-17-12-10-38.png)\n\n4. Again it will look at the IoU of this box with the remaining boxes and compress the boxes with a high IoU:\n![](https://cdn.analyticsvidhya.com/wp-content/uploads/2018/12/Screenshot-from-2018-11-17-12-11-35.png)\n\n5. We repeat these steps until all the boxes have either been selected or compressed and we get the final bounding boxes:\n![](https://cdn.analyticsvidhya.com/wp-content/uploads/2018/12/Screenshot-from-2018-11-17-12-21-31.png)\n\n\nLet's now write some utility functions to get our predictions back to bboxes and implementing the Non max supression algorithm we had just discussed:","execution_count":null},{"metadata":{"trusted":true},"cell_type":"code","source":"def prediction_to_bbox(Bboxes, image_grid): \n    '''A function to convert predictions of Model from scaled YOLO back to COCO BBoxes'''\n    \n    bboxes = Bboxes.copy()\n    \n    # mutiplying with the number of grids\n    im_width = (image_grid[:,:,2] * GRID_SIZE)\n    im_height = (image_grid[:,:,3] * GRID_SIZE)\n    \n    # descale X, Y, HEIGHT, WIDTH\n    for i in range(ANCHORS):\n        bboxes[:,:, (5*i)+1] = (bboxes[:,:, (5*i)+1] * image_grid[:,:,2]) + image_grid[:,:,0]\n        bboxes[:,:, (5*i)+2] = (bboxes[:,:, (5*i)+2] * image_grid[:,:,3]) + image_grid[:,:,1]\n        bboxes[:,:, (5*i)+3] = (bboxes[:,:, (5*i)+3] * im_width)\n        bboxes[:,:, (5*i)+4] = (bboxes[:,:, (5*i)+4] * im_height)\n        \n    \n    # centre x,y to top left x,y\n    for i in range(ANCHORS):\n        bboxes[:,:, (5*i)+1] = bboxes[:,:, (5*i)+1] - (bboxes[:,:, (5*i)+3] / 2)\n        bboxes[:,:, (5*i)+2] = bboxes[:,:, (5*i)+2] - (bboxes[:,:, (5*i)+4] / 2)\n\n    \n    # width,heigth to X_MAX,Y_MAX\n    # this is important since we would be feeding\n    # bboxes to tf.image.non_max_supression\n    for i in range(ANCHORS):\n        bboxes[:,:, (5*i)+3] = bboxes[:,:, (5*i)+1] + bboxes[:,:, (5*i)+3]\n        bboxes[:,:, (5*i)+4] = bboxes[:,:, (5*i)+2] + bboxes[:,:, (5*i)+4]\n    \n    return bboxes\n\ndef non_max_suppression(predictions, nms_thresh, nms_score, top_n=10):\n    \n    probabilities = np.concatenate([predictions[:,:, 5*i].flatten() for i in range(ANCHORS)], axis=None)\n    \n    # having extracted probs, we extract just x,y,h,w from two anchors\n    first_anchors = predictions[:, :, 1:5].reshape((GRID_SIZE * GRID_SIZE, 4))\n    second_anchors = predictions[:, :, 6:10].reshape((GRID_SIZE * GRID_SIZE, 4))\n    \n    # we concatenate both anchor boxes together\n    bboxes = np.concatenate(\n        [predictions[:, :, (5*i)+1:(5*i)+5].reshape((GRID_SIZE * GRID_SIZE, 4)) for i in range(ANCHORS)],\n        axis=0\n    )\n    \n    # given the probablities of bounding boxes, we perform NMS and return \n    # only bboxes above threshold IOU bboxes with high similarity are\n    # removed and replaced with one with the highest conf\n    # input is supplied as [y1, x1, y2, x2]\n    top_indices = tf.image.non_max_suppression(\n        boxes=bboxes[:, [1, 0, 3, 2]], \n        scores=probabilities, \n        max_output_size=top_n, \n        iou_threshold=nms_thresh,\n        score_threshold=nms_score,\n    ).numpy()\n    \n    return np.hstack([probabilities[top_indices][..., np.newaxis], bboxes[top_indices]])\n\ndef process_predictions(Predictions, image_ids, image_grid, return_prob=False, nms_thresh=0.3, nms_score=0.3, rescale=False):\n    '''Returns a dictionary. BBoxes as values & image_ids are keys.'''\n    \n    predictions = Predictions.copy()\n    bboxes = {}\n    \n    for i, image_id in enumerate(image_ids):\n        \n        # convert YOLO to COCO\n        predictions[i] = prediction_to_bbox(predictions[i], image_grid)\n        # NMS, Keep only top 100 predictions\n        bboxes[image_id] = non_max_suppression(predictions[i], top_n=100, nms_thresh=nms_thresh, nms_score=nms_score)\n        \n        # back to coco (x, y, X_MAX, y_MAX) -> (x, y, width, height)\n        bboxes[image_id][:, 3:5] = bboxes[image_id][:, 3:5] - bboxes[image_id][:, 1:3]\n        \n        if not return_prob:\n            bboxes[image_id] = bboxes[image_id][:, 1:]\n        \n        if rescale:\n            bboxes[image_id][:, -4:] = bboxes[image_id][:, -4:] * FACTOR\n        \n    return bboxes","execution_count":null,"outputs":[]},{"metadata":{},"cell_type":"markdown","source":"Lets see if our process_predictions pipeline work as expected:","execution_count":null},{"metadata":{"trusted":true},"cell_type":"code","source":"img = []\nval_predictions = []\ntemp = val_generator[np.random.randint(0, len(val_generator))]\n\nfor i in tqdm(range(8)):\n    \n    img.append(temp[0][i])\n    val_predictions.append(temp[1][i])\n    \nval_predictions = process_predictions(val_predictions, range(8), image_grid)","execution_count":null,"outputs":[]},{"metadata":{"trusted":true},"cell_type":"code","source":"f, ax, = plt.subplots(nrows=4, ncols=2, figsize=(12, 24))\nax = ax.ravel()\n\nfor i in range(8):\n    \n    ax[i].imshow(draw_bboxes((img[i] * 255).astype(\"uint8\"), \n                             val_predictions[i], img_id_as_input=False, \n                             color='red', W=2))\n    \n    ax[i].axis('off')","execution_count":null,"outputs":[]},{"metadata":{},"cell_type":"markdown","source":"Working good! Now let's create a tensorflow dataset for these generators to be able to cache it to memory:","execution_count":null},{"metadata":{"trusted":true},"cell_type":"code","source":"with strategy.scope():\n    \n    val_dataset = tf.data.Dataset.from_generator(\n        lambda: GWD_Dataset(val_img_ids,\n            train_files,\n            labels/FACTOR, \n            batch_size=BATCH_SIZE,\n            shuffle=False,\n            augment=False),\n        output_types=(tf.float32, tf.float32),\n        output_shapes=([BATCH_SIZE, *IMG_SHAPE, 3], [BATCH_SIZE, GRID_SIZE, GRID_SIZE, 5*ANCHORS])\n    ).cache()\n\nval_dataset","execution_count":null,"outputs":[]},{"metadata":{"trusted":true},"cell_type":"code","source":"%time temp = next(iter(val_dataset))","execution_count":null,"outputs":[]},{"metadata":{},"cell_type":"markdown","source":"Let's now define our model.\n\nWe are going to use the yolo_v3 model:","execution_count":null},{"metadata":{"trusted":true},"cell_type":"code","source":"tf.keras.backend.clear_session()","execution_count":null,"outputs":[]},{"metadata":{"trusted":true},"cell_type":"code","source":"# with strategy.scope():\n    \nx_input = tf.keras.Input(shape=(*IMG_SHAPE, 3))\n\nx = tf.keras.layers.Conv2D(32, (3, 3), strides=(1, 1), padding='same')(x_input)\nx = tf.keras.layers.BatchNormalization()(x)\nx = tf.keras.layers.LeakyReLU(alpha=0.1)(x)\n\n########## block 1 ##########\nx = tf.keras.layers.Conv2D(64, (3, 3), strides=(1, 1), padding='same')(x)\nx = tf.keras.layers.BatchNormalization()(x)\nx = tf.keras.layers.LeakyReLU(alpha=0.1)(x)\n\nx_shortcut = x\n\nfor i in range(2):\n    x = tf.keras.layers.Conv2D(32, (3, 3), strides=(1, 1), padding='same')(x)\n    x = tf.keras.layers.BatchNormalization()(x)\n    x = tf.keras.layers.LeakyReLU(alpha=0.1)(x)\n\n    x = tf.keras.layers.Conv2D(64, (3, 3), strides=(1, 1), padding='same')(x)\n    x = tf.keras.layers.BatchNormalization()(x)\n    x = tf.keras.layers.LeakyReLU(alpha=0.1)(x)\n\n    x = tf.keras.layers.Add()([x_shortcut, x])\n    x = tf.keras.layers.LeakyReLU(alpha=0.1)(x)\n\n    x_shortcut = x\n\n\n########## block 2 ##########\nx = tf.keras.layers.Conv2D(128, (3, 3), strides=(2, 2), padding='same')(x)\nx = tf.keras.layers.BatchNormalization()(x)\nx = tf.keras.layers.LeakyReLU(alpha=0.1)(x)\n\nx_shortcut = x\n\nfor i in range(2):\n    x = tf.keras.layers.Conv2D(64, (3, 3), strides=(1, 1), padding='same')(x)\n    x = tf.keras.layers.BatchNormalization()(x)\n    x = tf.keras.layers.LeakyReLU(alpha=0.1)(x)\n\n    x = tf.keras.layers.Conv2D(128, (3, 3), strides=(1, 1), padding='same')(x)\n    x = tf.keras.layers.BatchNormalization()(x)\n    x = tf.keras.layers.LeakyReLU(alpha=0.1)(x)\n\n    x = tf.keras.layers.Add()([x_shortcut, x])\n    x = tf.keras.layers.LeakyReLU(alpha=0.1)(x)\n\n    x_shortcut = x\n\n########## block 3 ##########\nx = tf.keras.layers.Conv2D(256, (3, 3), strides=(2, 2), padding='same')(x)\nx = tf.keras.layers.BatchNormalization()(x)\nx = tf.keras.layers.LeakyReLU(alpha=0.1)(x)\n\nx_shortcut = x\n\nfor i in range(8):\n    x = tf.keras.layers.Conv2D(128, (3, 3), strides=(1, 1), padding='same')(x)\n    x = tf.keras.layers.BatchNormalization()(x)\n    x = tf.keras.layers.LeakyReLU(alpha=0.1)(x)\n\n    x = tf.keras.layers.Conv2D(256, (3, 3), strides=(1, 1), padding='same')(x)\n    x = tf.keras.layers.BatchNormalization()(x)\n    x = tf.keras.layers.LeakyReLU(alpha=0.1)(x)\n\n    x = tf.keras.layers.Add()([x_shortcut, x])\n    x = tf.keras.layers.LeakyReLU(alpha=0.1)(x)\n\n    x_shortcut = x\n\n\n########## block 4 ##########\nx = tf.keras.layers.Conv2D(512, (3, 3), strides=(2, 2), padding='same')(x)\nx = tf.keras.layers.BatchNormalization()(x)\nx = tf.keras.layers.LeakyReLU(alpha=0.1)(x)\n\nx_shortcut = x\n\nfor i in range(8):\n    x = tf.keras.layers.Conv2D(256, (3, 3), strides=(1, 1), padding='same')(x)\n    x = tf.keras.layers.BatchNormalization()(x)\n    x = tf.keras.layers.LeakyReLU(alpha=0.1)(x)\n\n    x = tf.keras.layers.Conv2D(512, (3, 3), strides=(1, 1), padding='same')(x)\n    x = tf.keras.layers.BatchNormalization()(x)\n    x = tf.keras.layers.LeakyReLU(alpha=0.1)(x)\n\n    x = tf.keras.layers.Add()([x_shortcut, x])\n    x = tf.keras.layers.LeakyReLU(alpha=0.1)(x)\n    \n\n########## output layers ##########\nx = tf.keras.layers.Conv2D(512, (3, 3), strides=(1, 1), padding='same')(x)\nx = tf.keras.layers.BatchNormalization()(x)\nx = tf.keras.layers.LeakyReLU(alpha=0.1)(x)\n\nx = tf.keras.layers.Conv2D(256, (3, 3), strides=(1, 1), padding='same')(x)\nx = tf.keras.layers.BatchNormalization()(x)\nx = tf.keras.layers.LeakyReLU(alpha=0.1)(x)\n\nx = tf.keras.layers.Conv2D(128, (3, 3), strides=(1, 1), padding='same')(x)\nx = tf.keras.layers.BatchNormalization()(x)\nx = tf.keras.layers.LeakyReLU(alpha=0.1)(x)\n\npredictions = tf.keras.layers.Conv2D(5*ANCHORS, (1, 1), strides=(1, 1), activation='sigmoid')(x)\n\nmodel = tf.keras.Model(inputs=x_input, outputs=predictions)","execution_count":null,"outputs":[]},{"metadata":{"trusted":true},"cell_type":"code","source":"model.output_shape","execution_count":null,"outputs":[]},{"metadata":{},"cell_type":"markdown","source":"'''\n\n\nOne issue with yolo is that it is likely to contain more cells in its label grid that contain no objects than cells that do contain objects. It is easy then for the model to focus too much on learning to reduce no object cells to zero and not focus enough on getting the bounding boxes to the right shape. To overcome this the yolo paper suggests _weighting the cells containing bounding boxes five times higher and the cells with no bounding boxes by half._\n\nI have defined a custom loss function to do just this. I have also split the loss function into three parts. The first takes care of the confidence score that is trying to work out if a label grid cell contains a head of wheat or not. Binary cross entropy is used here as that is a binary classification task. The second part looks at the x,y position of the bounding boxes while the third looks at the width,height of the bounding boxes. MSE (mean squared loss) is used for the second and third parts as they are regression tasks.\n\n'''","execution_count":null},{"metadata":{"trusted":true},"cell_type":"code","source":"def custom_loss(y_true, y_pred):\n    binary_crossentropy = tf.keras.losses.BinaryCrossentropy(\n        reduction=tf.keras.losses.Reduction.SUM_OVER_BATCH_SIZE\n#         reduction=tf.keras.losses.Reduction.NONE\n    )\n    \n    # binary cross entropy for confidence scores\n    prob_loss = binary_crossentropy(\n        tf.concat([y_true[:,:,:,5*i] for i in range(ANCHORS)], axis=0), \n        tf.concat([y_pred[:,:,:,5*i] for i in range(ANCHORS)], axis=0)\n    )\n    \n    # MSE for x, y (TWO anchor boxes)\n    xy_loss = tf.keras.losses.MSE(\n        tf.concat([y_true[:,:,:,(5*i)+1:(5*i)+3] for i in range(ANCHORS)], axis=0),\n        tf.concat([y_pred[:,:,:,(5*i)+1:(5*i)+3] for i in range(ANCHORS)], axis=0)\n    )\n    \n    # MSE loss for Height and width (TWO anchor boxes)\n    wh_loss = tf.keras.losses.MSE(\n        tf.concat([y_true[:,:,:,(5*i)+3:(5*i)+5] for i in range(ANCHORS)], axis=0),\n        tf.concat([y_pred[:,:,:,(5*i)+3:(5*i)+5] for i in range(ANCHORS)], axis=0)\n    )\n    \n    # get mask for appropriate weighing of the losses\n    bboxes_mask = get_mask(y_true)\n    weighted_loss = (xy_loss + wh_loss) * bboxes_mask\n    \n#     return tf.reduce_mean(prob_loss) + weighted_loss\n    return prob_loss + weighted_loss\n\n\ndef get_mask(y_true):\n    '''If the grid cell contains a wheat head we need to weigh it \n    5 times higher and 0.5 times if it contains no wheat cell.'''\n    \n    bboxes_mask = tf.concat(\n        [tf.where(y_true[:,:,:,5*i] == 0, 0.5, 5.0) for i in range(ANCHORS)],\n        axis=0\n    )\n    \n    return bboxes_mask","execution_count":null,"outputs":[]},{"metadata":{"trusted":true},"cell_type":"code","source":"# with strategy.scope():\n\nmodel.compile(\n    optimizer=tf.keras.optimizers.Adam(learning_rate=0.0001), \n    loss=custom_loss\n)\n\ncallbacks = [\n    tf.keras.callbacks.ReduceLROnPlateau(monitor='val_loss', patience=2, factor=0.1),\n    tf.keras.callbacks.EarlyStopping(monitor='val_loss', patience=4, restore_best_weights=True),\n    tf.keras.callbacks.ModelCheckpoint(\"./model_save\", save_best_only=True),\n]","execution_count":null,"outputs":[]},{"metadata":{"trusted":true},"cell_type":"code","source":"history = model.fit(\n    train_generator,\n    validation_data=val_dataset,\n    epochs=50,\n    callbacks=callbacks\n)","execution_count":null,"outputs":[]},{"metadata":{"trusted":true},"cell_type":"code","source":"model.save(\"model.h5\")","execution_count":null,"outputs":[]},{"metadata":{},"cell_type":"markdown","source":"Let's visualize and evaluate our model performance:","execution_count":null},{"metadata":{"trusted":true},"cell_type":"code","source":"fig, ax = plt.subplots(1, 2, figsize=(20, 5))\n\nax[0].set_title('Training Loss')\nax[0].plot(history.history['loss'])\n\nax[1].set_title('Validation Loss')\nax[1].plot(history.history['val_loss'])\n\nprint('Epochs: ' + str(len(history.history['loss'])))\nprint('Final training loss: ' + str(history.history['loss'][-1]))\nprint('Final validation loss: ' + str(history.history['val_loss'][-1]))","execution_count":null,"outputs":[]},{"metadata":{"trusted":true},"cell_type":"code","source":"val_predictions = model.predict(val_dataset)\nval_predictions = process_predictions(\n    val_predictions, val_img_ids[:len(val_generator) * BATCH_SIZE], \n    image_grid, nms_thresh=0.25, nms_score=0.35)","execution_count":null,"outputs":[]},{"metadata":{},"cell_type":"markdown","source":"Lets visualize the predictions the model has made on the validation data:","execution_count":null},{"metadata":{"trusted":true},"cell_type":"code","source":"temp = np.random.choice(val_img_ids[:len(val_generator) * BATCH_SIZE], (8,))\n\n# plotting for validation images\nf, ax = plt.subplots(nrows=2, ncols=4, figsize=(20, 10))\nax = ax.ravel()\n\nfor i, img_id in enumerate(temp):\n    org = draw_bboxes(img_id, labels[img_id], color='blue')\n    ax[i].imshow(draw_bboxes(org, val_predictions[img_id]*FACTOR, img_id_as_input=False, color='red'))\n    ax[i].axis('off')\n    \nf.suptitle(\"Model Evaluation\", size=20)\nf.tight_layout(rect=[0, 0.03, 1, 0.95]);","execution_count":null,"outputs":[]},{"metadata":{},"cell_type":"markdown","source":"Let's make predictions on the test images:","execution_count":null},{"metadata":{"trusted":true},"cell_type":"code","source":"test_image_ids = os.listdir('/kaggle/input/global-wheat-detection/test/')\ntest_image_ids = [image_id[:-4] for image_id in test_image_ids]\n\ntest_predictions = []\n\nfor i, image_id in enumerate(test_image_ids):\n    image = Image.open('/kaggle/input/global-wheat-detection/test/' + image_id + \".jpg\")\n    image = image.resize(IMG_SHAPE)            \n\n    pixels = np.asarray(image)\n\n    aug_result = VAL_AUG(image=pixels)\n    pixels = np.array(aug_result['image']) / 255\n    pixels = np.expand_dims(pixels, axis=0)\n\n    bboxes = model.predict(pixels)\n\n    test_predictions.append(bboxes)\n    \ntest_predictions = np.concatenate(test_predictions)\ntest_predictions = process_predictions(\n    test_predictions, test_image_ids, image_grid, \n    return_prob=True, nms_thresh=0.2, nms_score=0.5)","execution_count":null,"outputs":[]},{"metadata":{"trusted":true},"cell_type":"code","source":"# plotting for test images\nf, ax = plt.subplots(nrows=3, ncols=3, figsize=(15, 15))\nax = ax.ravel()\n\nfor i, img_id in enumerate(test_image_ids[:9]):\n    ax[i].imshow(draw_bboxes(img_id, test_predictions[img_id][:, 1:]*FACTOR, source='test'))\n    ax[i].axis('off')","execution_count":null,"outputs":[]},{"metadata":{"trusted":true},"cell_type":"code","source":"op = pd.DataFrame(test_predictions.items())\nop[1] = op[1].map(lambda x: ' '.join(np.ravel(x).astype(str)))\nop.rename({0: \"image_id\", 1: \"PredictionString\"}, axis=1).to_csv(\"submission.csv\", index=False)\npd.read_csv(\"submission.csv\")","execution_count":null,"outputs":[]},{"metadata":{},"cell_type":"markdown","source":"How to improve on our model predictions?\n\nYOLO does not work pretty well for small objects. In order to improve its performance on smaller objects, you can try the following things:\n\n- Increase the number of anchor boxes\n- Decrease the threshold for IoU\n- Give wh_loss more weights than xy_loss since our model gets the height and width predictions wrong\n- Transfer learning: Use pretrained weights: https://github.com/jacksonxliu/YOLOv3-tiny-custom-object-detection","execution_count":null}],"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}