{"cells":[{"metadata":{"_uuid":"8d84f5d2fe3e7ed64cc7c6bffde94115a117e373"},"cell_type":"markdown","source":"In this kernel we will slightly extend the ideas from https://www.kaggle.com/voglinio/from-masks-to-bounding-boxes in order to create *rotated* bounding boxes. We will define rotation angle in degrees in a range between 0 and 180, without taking account the orientation of the ship (aft and bow). This script will create a rotated bounding box `center_x, center_y, size_x, size_y, angle)`."},{"metadata":{"_uuid":"8f2839f25d086af736a60e9eeb907d3b93b6e0e5","_cell_guid":"b1076dfc-b9ad-4769-8c92-a6c4dae69d19","trusted":true},"cell_type":"code","source":"import os\nimport cv2\nfrom tqdm import tqdm\nimport numpy as np # linear algebra\nimport pandas as pd # data processing, CSV file I/O (e.g. pd.read_csv)\nfrom skimage.io import imread\nimport matplotlib.pyplot as plt\nfrom skimage.segmentation import mark_boundaries\nfrom skimage.measure import label, regionprops\nship_dir = '../input'\ntrain_image_dir = os.path.join(ship_dir, 'train')\ntest_image_dir = os.path.join(ship_dir, 'test')\n\nfrom skimage.morphology import label\ndef multi_rle_encode(img):\n    labels = label(img[:, :, 0])\n    return [rle_encode(labels==k) for k in np.unique(labels[labels>0])]\n\n# ref: https://www.kaggle.com/paulorzp/run-length-encode-and-decode\ndef rle_encode(img):\n    '''\n    img: numpy array, 1 - mask, 0 - background\n    Returns run length as string formated\n    '''\n    pixels = img.T.flatten()\n    pixels = np.concatenate([[0], pixels, [0]])\n    runs = np.where(pixels[1:] != pixels[:-1])[0] + 1\n    runs[1::2] -= runs[::2]\n    return ' '.join(str(x) for x in runs)\n\ndef rle_decode(mask_rle, shape=(768, 768)):\n    '''\n    mask_rle: run-length as string formated (start length)\n    shape: (height,width) of array to return \n    Returns numpy array, 1 - mask, 0 - background\n    '''\n    s = mask_rle.split()\n    starts, lengths = [np.asarray(x, dtype=int) for x in (s[0:][::2], s[1:][::2])]\n    starts -= 1\n    ends = starts + lengths\n    img = np.zeros(shape[0]*shape[1], dtype=np.uint8)\n    for lo, hi in zip(starts, ends):\n        img[lo:hi] = 1\n    return img.reshape(shape).T  # Needed to align to RLE direction\n\ndef masks_as_image(in_mask_list, all_masks=None):\n    # Take the individual ship masks and create a single mask array for all ships\n    if all_masks is None:\n        all_masks = np.zeros((768, 768), dtype = np.int16)\n    #if isinstance(in_mask_list, list):\n    for mask in in_mask_list:\n        if isinstance(mask, str):\n            all_masks += rle_decode(mask)\n    return np.expand_dims(all_masks, -1)","execution_count":null,"outputs":[]},{"metadata":{"_uuid":"482b8891d20ac2b8052d77ee58d0f766aba7d674"},"cell_type":"markdown","source":"Let us read the masks:"},{"metadata":{"trusted":true,"_uuid":"24bcb040514e697fd80f03291d322b73146bceda"},"cell_type":"code","source":"masks = pd.read_csv(os.path.join('../input/',\n                                 'train_ship_segmentations.csv'))\nprint(masks.shape[0], 'masks found')\nprint(masks['ImageId'].value_counts().shape[0])\nmasks.head()","execution_count":null,"outputs":[]},{"metadata":{"_uuid":"15feac3041c7f644fa1afc39478abfbab380583d"},"cell_type":"markdown","source":"and keep only those that contain ships. Keep in mind that image files can be repeated many times in the csv file. So a unique operator will give us the unique filenames that contain ships."},{"metadata":{"_cell_guid":"79c7e3d0-c299-4dcb-8224-4455121ee9b0","_uuid":"d629ff2d2480ee46fbb7e2d37f6b5fab8052498a","trusted":true},"cell_type":"code","source":"images_with_ship = masks.ImageId[masks.EncodedPixels.isnull()==False]\nimages_with_ship = np.unique(images_with_ship.values)\nprint('There are ' +str(len(images_with_ship)) + ' image files with masks')","execution_count":null,"outputs":[]},{"metadata":{"_uuid":"3385f11ac14a1b32559133e23498984f147a89ef"},"cell_type":"markdown","source":"In order to extract the bounding box we:\n1. Load mask as binary numpy array using Kevin's `masks_as_image`)\n\n2. Label  connected regions of this mask using `skimage.measure.label`\n\n3. Measure morphological properties of these connected regions and keep the bounding box (`skimage.measure.regionprops`). For each connected region a bounding box we will use a combination of `cv2.findContours` and `cv2.minAreaRect`. To extend the angle in the range [0, 180] we perform a simple test that finds the longest of the two sizes and adapts the angle accordingly.\n\n(*Note: I aven't fully tested the results. Any help on that would be welcome* )\n\nLet us view some  examples:"},{"metadata":{"trusted":true,"scrolled":false,"_uuid":"9b1ee6524f6eba8bb21921a609dfcfd2fabbf114"},"cell_type":"code","source":"plotme = 1\n\nfor i in tqdm(range(0, 10)):\n    image = images_with_ship[i]\n\n    if plotme == 1:\n        fig, (ax1, ax2, ax3) = plt.subplots(1, 3, figsize = (15, 5))\n    img_0 = cv2.imread(train_image_dir+'/' + image)\n    rle_0 = masks.query('ImageId==\"'+image+'\"')['EncodedPixels']\n    mask_0 = masks_as_image(rle_0)\n    #\n    # \n    lbl_0, lbl_cnt = label(mask_0, return_num=True) \n    #props = regionprops(lbl_0)\n    img_1 = img_0.copy()\n    #print ('Image', image, lbl_cnt)\n    for i in range(1, lbl_cnt+1):\n        mask = np.array((lbl_0 == i).astype('uint8')[..., 0])\n        mask = cv2.resize(mask, (768, 768))\n        _, cnts, hierarchy = cv2.findContours((255*mask).astype('uint8'), cv2.RETR_TREE, cv2.CHAIN_APPROX_SIMPLE)\n        rect = cv2.minAreaRect(cnts[0])\n        if rect[1][1]>rect[1][0]:\n            angle = 90-rect[2]\n        else:\n            angle = -rect[2]\n        box = cv2.boxPoints(rect)\n        #print (box)\n        box = np.int0(box)\n\n        if plotme == 1:\n            cv2.drawContours(img_1,[box],0,(0,191,255),2)\n        x = int(rect[0][0])\n        y = int(rect[0][1])\n        #print (rect, angle, 360*((props[i-1].orientation + np.pi/2)/(2*np.pi)), x, y)\n        if plotme == 1:\n            cv2.circle(img_1, ( x, y ), 5, (255, 0, 0), 3)\n        print(str(rect[0][0]) + ' ' + str(rect[0][1]) + ' ' + str(rect[1][0]) + ' ' + str(rect[1][1]) + ' ' + '1' + ' ' + str(angle) + '\\n' )\n\n    if plotme == 1:\n        ax1.imshow(img_0)\n        ax1.set_title('Image')\n        ax2.set_title('Mask')\n        ax3.set_title('Image with derived bounding box')\n        ax2.imshow(mask_0[...,0], cmap='gray')\n        ax3.imshow(img_1)\n        plt.show()","execution_count":null,"outputs":[]},{"metadata":{"_uuid":"8b1c568de89f266ab84eaeecb972e644688ff017"},"cell_type":"markdown","source":"Here we calculate the rotated bounding boxes for all `29070` images and save then into a dictionary. "},{"metadata":{"trusted":true,"_uuid":"32b980a591b51e25a29809bb3e7c2a00e09666b7"},"cell_type":"code","source":"plotme = 0\nrot_bboxes_dict = {}\n\nfor i in tqdm(range(0, len(images_with_ship))):\n    image = images_with_ship[i]\n\n    if plotme == 1:\n        fig, (ax1, ax2, ax3) = plt.subplots(1, 3, figsize = (15, 5))\n    img_0 = cv2.imread(train_image_dir+'/' + image)\n    rle_0 = masks.query('ImageId==\"'+image+'\"')['EncodedPixels']\n    mask_0 = masks_as_image(rle_0)\n    #\n    # \n    lbl_0, lbl_cnt = label(mask_0, return_num=True) \n    #props = regionprops(lbl_0)\n    img_1 = img_0.copy()\n    bboxes = []\n    for i in range(1, lbl_cnt+1):\n        mask = np.array((lbl_0 == i).astype('uint8')[..., 0])\n        mask = cv2.resize(mask, (768, 768))\n        _, cnts, hierarchy = cv2.findContours((255*mask).astype('uint8'), cv2.RETR_TREE, cv2.CHAIN_APPROX_SIMPLE)\n        rect = cv2.minAreaRect(cnts[0])\n        if rect[1][1]>rect[1][0]:\n            angle = 90-rect[2]\n        else:\n            angle = -rect[2]\n        box = cv2.boxPoints(rect)\n        #print (box)\n        box = np.int0(box)\n\n        if plotme == 1:\n            cv2.drawContours(img_1,[box],0,(0,191,255),2)\n        x = int(rect[0][0])\n        y = int(rect[0][1])\n        if plotme == 1:\n            cv2.circle(img_1, ( x, y ), 5, (255, 0, 0), 3)\n        bboxes.append([rect[0][0], rect[0][1], rect[1][0], rect[1][1], angle])\n        \n    rot_bboxes_dict[image] = bboxes.copy()\n    if plotme == 1:\n        ax1.imshow(img_0)\n        ax1.set_title('Image')\n        ax2.set_title('Mask')\n        ax3.set_title('Image with derived bounding box')\n        ax2.imshow(mask_0[...,0], cmap='gray')\n        ax3.imshow(img_1)\n        plt.show()","execution_count":null,"outputs":[]},{"metadata":{"_uuid":"6ec23497a3ac0b5503e9148d53d588c342146535"},"cell_type":"markdown","source":"Export these bounding boxes for everyone to use in a Pandas dataframe."},{"metadata":{"trusted":true,"_uuid":"8b121c9848b425d6e6433c58dab0553abb6761dc"},"cell_type":"code","source":"bboxes_df = pd.DataFrame([rot_bboxes_dict])\nbboxes_df = bboxes_df.transpose()\nbboxes_df.columns = ['bbox_list']\nbboxes_df.head()","execution_count":null,"outputs":[]},{"metadata":{"trusted":true,"_uuid":"f911cbaef310eb0a649958f64dfeb94fce7035c8"},"cell_type":"code","source":"bboxes_df.to_csv('rotated_bbox_dictionary.csv')","execution_count":null,"outputs":[]},{"metadata":{"trusted":true,"collapsed":true,"_uuid":"a5c382017f4e914cee5b5ab0644ef0ec881b306f"},"cell_type":"code","source":" ","execution_count":null,"outputs":[]}],"metadata":{"kernelspec":{"display_name":"Python 3","language":"python","name":"python3"},"language_info":{"name":"python","version":"3.6.6","mimetype":"text/x-python","codemirror_mode":{"name":"ipython","version":3},"pygments_lexer":"ipython3","nbconvert_exporter":"python","file_extension":".py"}},"nbformat":4,"nbformat_minor":1}