{"metadata":{"kernelspec":{"language":"python","display_name":"Python 3","name":"python3"},"language_info":{"pygments_lexer":"ipython3","nbconvert_exporter":"python","version":"3.6.4","file_extension":".py","codemirror_mode":{"name":"ipython","version":3},"name":"python","mimetype":"text/x-python"}},"nbformat_minor":4,"nbformat":4,"cells":[{"cell_type":"code","source":"","metadata":{},"execution_count":null,"outputs":[]},{"cell_type":"markdown","source":"# <a id=\"imports\"></a>Installation and imports\n\nWe need to install the `rasterio` package to vectorize the raster masks into polygons.","metadata":{}},{"cell_type":"code","source":"!pip install rasterio","metadata":{"execution":{"iopub.status.busy":"2023-05-02T09:56:24.574896Z","iopub.execute_input":"2023-05-02T09:56:24.575344Z","iopub.status.idle":"2023-05-02T09:56:38.139764Z","shell.execute_reply.started":"2023-05-02T09:56:24.575304Z","shell.execute_reply":"2023-05-02T09:56:38.138293Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"code","source":"import pandas as pd\nimport numpy as np\nfrom PIL import Image\nimport os\nimport matplotlib.pyplot as plt\nimport matplotlib.patches as patches\nimport math\n\n%matplotlib inline","metadata":{"_cell_guid":"b1076dfc-b9ad-4769-8c92-a6c4dae69d19","_uuid":"8f2839f25d086af736a60e9eeb907d3b93b6e0e5","execution":{"iopub.status.busy":"2023-05-02T09:56:38.142234Z","iopub.execute_input":"2023-05-02T09:56:38.142617Z","iopub.status.idle":"2023-05-02T09:56:38.153083Z","shell.execute_reply.started":"2023-05-02T09:56:38.142572Z","shell.execute_reply":"2023-05-02T09:56:38.151777Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"code","source":"# function to convert RLE mask into 2d pixel array\ndef encode_mask(mask, shape=(768,768)):\n    img = np.zeros(shape[0]*shape[1], dtype=np.uint8)\n    s = mask.split()\n    for i in range(len(s)//2):\n        start = int(s[2*i]) - 1\n        length = int(s[2*i+1])\n        img[start:start+length] = 1\n    return img.reshape(shape).T","metadata":{"execution":{"iopub.status.busy":"2023-05-02T09:56:38.154528Z","iopub.execute_input":"2023-05-02T09:56:38.154888Z","iopub.status.idle":"2023-05-02T09:56:38.164434Z","shell.execute_reply.started":"2023-05-02T09:56:38.154848Z","shell.execute_reply":"2023-05-02T09:56:38.162909Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"code","source":"# where is stored the source data\nIMG_PATH = '../input/airbus-ship-detection/train_v2/'\nRLE_PATH = '../input/airbus-ship-detection/train_ship_segmentations_v2.csv'","metadata":{"_uuid":"77d824c0448a6c2022d342b4c0d2b352f0207377","execution":{"iopub.status.busy":"2023-05-02T09:56:38.166923Z","iopub.execute_input":"2023-05-02T09:56:38.167991Z","iopub.status.idle":"2023-05-02T09:56:38.175846Z","shell.execute_reply.started":"2023-05-02T09:56:38.167764Z","shell.execute_reply":"2023-05-02T09:56:38.174647Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"markdown","source":"## Skip processing and jump to illustrations\n<a href=#check>Click here</a> to skip the actual processing of RLE to OBBOX and jump directly to examples.","metadata":{}},{"cell_type":"code","source":"# read the RLE annotation in a Pandas DataFrame\nrle_df = pd.read_csv(RLE_PATH)\nrle_df.head()","metadata":{"execution":{"iopub.status.busy":"2023-05-02T09:56:38.179296Z","iopub.execute_input":"2023-05-02T09:56:38.179680Z","iopub.status.idle":"2023-05-02T09:56:39.776635Z","shell.execute_reply.started":"2023-05-02T09:56:38.179634Z","shell.execute_reply":"2023-05-02T09:56:39.774976Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"markdown","source":"# Processing annotations\n\nIn this section, we will convert:\n- RLE encoded strings into masks, \n- then from masks to polygons, \n- from polygons to rectangle envelopes \n- and finally to rotated bounding boxes","metadata":{}},{"cell_type":"markdown","source":"This section is the core of the transformation. You should be able to follow each step. We preserve the annotation in case the RLE is void so that we know that this image has no annotation for the training of the classifier. \n\n- We first turn each RLE string into a mask with a single annotation using the above function, \n- then we use [`rasterio.features.shapes`](https://rasterio.readthedocs.io/en/stable/api/rasterio.features.html) function to vectorize the annotation into a **GeoJSON** polygon\n- then we use [`shapely.minimum_rotated_rectangle`](https://shapely.readthedocs.io/en/stable/constructive.html) function to get the rectangle envelope of the polygon\n- and finally we use some simple trigonometry functions to compute center, size and angle of the oriented bounding box (source [here](https://datascience.stackexchange.com/questions/104200/how-to-convert-horizontal-bounding-box-coordinates-to-oriented-bounding-box-coor))","metadata":{}},{"cell_type":"code","source":"from tqdm import tqdm\nfrom rasterio import features\nfrom shapely.geometry import MultiPoint\n\nlines = []\nfor row in tqdm(rle_df.itertuples(), total=len(rle_df)):\n    if pd.isna(row.EncodedPixels):\n        lines.append({\n            \"ImageId\": row.ImageId,\n            \"EncodedPixels\": np.nan,\n            \"xc\": np.nan,\n            \"yc\": np.nan, \n            \"dx\": np.nan,\n            \"dy\": np.nan,\n            \"angle\": np.nan\n        })\n        continue\n\n    # convert RLE encoded pixels into binary mask\n    mask = encode_mask(str(row.EncodedPixels), shape=(768,768))\n\n    # vectorize mask into GeoJSON\n    value = 0.0\n    for polygon, value in list(features.shapes(mask)):\n        if value == 1.0:\n            break\n    if value != 1.0:\n        print('Error while vectorizing mask')\n\n    # get oriented bounding box around shape\n    coords = polygon['coordinates'][0]\n    obbox = MultiPoint(coords).minimum_rotated_rectangle\n\n    # get center of bounding box and correct for half a pixel\n    xc, yc = list(obbox.centroid.coords)[0]\n    xc, yc = xc - 0.5, yc - 0.5\n    \n    # get external coordinates of oriented rectangle\n    # compute length, width and angle\n    p1, p2, p3, p4, p5 = list((obbox.exterior.coords))\n    dx = math.sqrt((p3[0] - p2[0])**2 + (p3[1] - p2[1])**2)\n    dy = math.sqrt((p2[0] - p1[0])**2 + (p2[1] - p1[1])**2)\n    angle = math.atan2((p3[1] - p2[1]), (p3[0] - p2[0]))\n    #length = max(dx, dy)\n    #height = min(dx, dy)\n    \n    # store in list\n    lines.append({\n        \"ImageId\": row.ImageId,\n        \"EncodedPixels\": row.EncodedPixels,\n        \"xc\": xc,\n        \"yc\": yc, \n        \"dx\": dx,\n        \"dy\": dy,\n        \"angle\": angle\n    })\n    #print(lines)\n    #break\n\n# create DataFrame\nobbox_df = pd.DataFrame(lines, columns=['ImageId', 'EncodedPixels', 'xc', 'yc', 'dx', 'dy', 'angle'])","metadata":{"execution":{"iopub.status.busy":"2023-05-02T09:56:39.778414Z","iopub.execute_input":"2023-05-02T09:56:39.778812Z","iopub.status.idle":"2023-05-02T10:11:13.641610Z","shell.execute_reply.started":"2023-05-02T09:56:39.778774Z","shell.execute_reply":"2023-05-02T10:11:13.640407Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"markdown","source":"The meaning of columns is the following:\n- `ImageId` is name of an image from train dataset (https://www.kaggle.com/c/airbus-ship-detection/data);\n- `EncodedPixels` is Run-Length Encoding of the mask; \n- `xc` and `yc` are X and Y coordinate of box center;\n- `dx` and `dy` are length (or distance) of the box along X and Y axis; \n- and `angle` is the rotation angle of the box in radians.\n\nThe values `dx` and `dy` do not correspond to length and width as we do not know which one is the longest (see specific commented code above). Also note that there are interesting discussions about the various ways to measure the orientation of the bounding box. Please check [this page](https://mmrotate.readthedocs.io/en/latest/intro.html).\n\nThe CSV file is stored as a new dataset [here](https://www.kaggle.com/datasets/jeffaudi/airbus-ships-annotations-oriented-bounding-boxes). ","metadata":{}},{"cell_type":"code","source":"# check the resulting DataFrame\nobbox_df.head()","metadata":{"execution":{"iopub.status.busy":"2023-05-02T10:11:13.642999Z","iopub.execute_input":"2023-05-02T10:11:13.643354Z","iopub.status.idle":"2023-05-02T10:11:13.659936Z","shell.execute_reply.started":"2023-05-02T10:11:13.643320Z","shell.execute_reply":"2023-05-02T10:11:13.658551Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"code","source":"# save the resulting dataframe on disk as a CSV\nobbox_df.to_csv(\"airbus_ship_train_obbox.csv\", \n                  sep=',', na_rep='', float_format=\"%.2f\", header=True, index=False)\n","metadata":{"execution":{"iopub.status.busy":"2023-05-02T10:11:13.661443Z","iopub.execute_input":"2023-05-02T10:11:13.661787Z","iopub.status.idle":"2023-05-02T10:11:15.909819Z","shell.execute_reply.started":"2023-05-02T10:11:13.661754Z","shell.execute_reply":"2023-05-02T10:11:15.908330Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"markdown","source":"# Checking results\n\nHere we will read again the oriented bounding boxes and define some display functions to visualize the result of the conversion.\n","metadata":{"_uuid":"6655029d983278f3d3af9294be12e19a8525eeb7"}},{"cell_type":"code","source":"# local file after processing\n#BOXES_PATH = '/kaggle/working/airbus_ship_train_obbox.csv'\n\n# access to file as a new dataset\nBOXES_PATH = '/kaggle/input/airbus-ships-annotations-oriented-bounding-boxes/airbus_ship_train_obbox.csv'\n\nbox_df = pd.read_csv(BOXES_PATH)\nbox_df.head()","metadata":{"_cell_guid":"79c7e3d0-c299-4dcb-8224-4455121ee9b0","_uuid":"d629ff2d2480ee46fbb7e2d37f6b5fab8052498a","scrolled":true,"execution":{"iopub.status.busy":"2023-05-02T10:11:15.911460Z","iopub.execute_input":"2023-05-02T10:11:15.911835Z","iopub.status.idle":"2023-05-02T10:11:18.281733Z","shell.execute_reply.started":"2023-05-02T10:11:15.911798Z","shell.execute_reply":"2023-05-02T10:11:18.280336Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"code","source":"#get bounding box for a mask\ndef get_bbox(img):\n    rows = np.any(img, axis=1)\n    cols = np.any(img, axis=0)\n    rmin, rmax = np.where(rows)[0][[0, -1]]\n    cmin, cmax = np.where(cols)[0][[0, -1]]\n    return rmin, rmax, cmin, cmax\n\n#add padding to the bounding box\ndef get_bbox_p(img, padding=5):\n    x1,x2,y1,y2 = get_bbox(img)\n    lx,ly = img.shape\n    x1 = max(x1-padding,0)\n    x2 = min(x2+padding+1, lx-1)\n    y1 = max(y1-padding,0)\n    y2 = min(y2+padding+1, ly-1)\n    return x1,x2,y1,y2\n\n#convert parameters of the box for plotting\ndef convert_box(box):\n    rot1 = math.cos(box[4])\n    rot2 = math.sin(box[4])\n    bx1 = box[0] - 0.5*(box[2]*rot1 - box[3]*rot2)\n    bx2 = box[1] - 0.5*(box[2]*rot2 + box[3]*rot1)\n    return (bx1,bx2,box[2],box[3],box[4]*180.0/math.pi)\n\ndef get_rec(box,width=1):\n    b = convert_box(box)\n    return patches.Rectangle((b[0],b[1]),b[2],b[3],b[4],linewidth=width,edgecolor='g',facecolor='none')","metadata":{"_uuid":"195a78c5e04f92631748bd9dd8dc2e7c6f18ab6e","execution":{"iopub.status.busy":"2023-05-02T10:11:18.286686Z","iopub.execute_input":"2023-05-02T10:11:18.287220Z","iopub.status.idle":"2023-05-02T10:11:18.303257Z","shell.execute_reply.started":"2023-05-02T10:11:18.287171Z","shell.execute_reply":"2023-05-02T10:11:18.301616Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"code","source":"#plot image, mask, zoomed image, and zoomed mask with rotating bounding boxes\ndef show_box(idx):\n    row = box_df.iloc[idx]\n    name, encoding, x, y, dx, dy, rot = row.ImageId, row.EncodedPixels, \\\n        row.xc, row.yc, row.dx, row.dy, row.angle\n    \n    image = np.asarray(Image.open(os.path.join(IMG_PATH,name)))\n        \n    if pd.isna(encoding): #empty image\n        plt.title(idx)\n        plt.imshow(image)\n        plt.axis('off')\n        return \n\n    mask = encode_mask(encoding)\n    box = (x,y,dx,dy,rot)\n\n    fig,ax = plt.subplots(2, 2, figsize=(16, 16))\n    ax[0,0].imshow(image)\n    ax[0,1].imshow(mask)\n    ax[0,0].add_patch(get_rec(box))\n    ax[0,1].add_patch(get_rec(box))\n    \n    y1,y2,x1,x2 = get_bbox_p(mask,10)\n    box_c = (x-x1,y-y1,dx,dy,rot)\n    ax[1,0].imshow(image[y1:y2,x1:x2,:])\n    ax[1,1].imshow(mask[y1:y2,x1:x2])\n    ax[1,0].add_patch(get_rec(box_c,3))\n    ax[1,1].add_patch(get_rec(box_c,3))\n    \n    for item in ax.flatten():\n        item.axis('off')\n\n    plt.title(idx)\n    plt.show()","metadata":{"_uuid":"1186970c8a9e46af384fcdde48824d6662f23cc9","execution":{"iopub.status.busy":"2023-05-02T10:11:18.305166Z","iopub.execute_input":"2023-05-02T10:11:18.305769Z","iopub.status.idle":"2023-05-02T10:11:18.321111Z","shell.execute_reply.started":"2023-05-02T10:11:18.305725Z","shell.execute_reply":"2023-05-02T10:11:18.319168Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"code","source":"import random\n\n# select a random image\nidx = random.randrange(len(box_df))\nshow_box(idx)","metadata":{"_uuid":"17d92079c4d1f14d7b0debbdeac89f69a32a1c14","execution":{"iopub.status.busy":"2023-05-02T10:11:18.322450Z","iopub.execute_input":"2023-05-02T10:11:18.322836Z","iopub.status.idle":"2023-05-02T10:11:18.772338Z","shell.execute_reply.started":"2023-05-02T10:11:18.322800Z","shell.execute_reply":"2023-05-02T10:11:18.771274Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"markdown","source":"# More examples of images with mask and oriented bounding box\n\nHere we generate an overview and a close view of a random annotation with the associated vector annotation and mask annotation.","metadata":{"_uuid":"b483b7306519a1070cd427624d6e938f31fe07a1"}},{"cell_type":"markdown","source":"And here are a few interesting examples.","metadata":{}},{"cell_type":"code","source":"show_box(222359)","metadata":{"execution":{"iopub.status.busy":"2023-05-02T10:11:18.773569Z","iopub.execute_input":"2023-05-02T10:11:18.774589Z","iopub.status.idle":"2023-05-02T10:11:19.493820Z","shell.execute_reply.started":"2023-05-02T10:11:18.774540Z","shell.execute_reply":"2023-05-02T10:11:19.492560Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"code","source":"show_box(11)","metadata":{"_uuid":"62229702bb14e9a7174dca21b4d70823ce867828","execution":{"iopub.status.busy":"2023-05-02T10:11:19.495300Z","iopub.execute_input":"2023-05-02T10:11:19.495900Z","iopub.status.idle":"2023-05-02T10:11:20.196254Z","shell.execute_reply.started":"2023-05-02T10:11:19.495864Z","shell.execute_reply":"2023-05-02T10:11:20.194773Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"code","source":"show_box(14326)","metadata":{"_uuid":"9a8f6868886b636687f8ef4503fd35d295a941df","execution":{"iopub.status.busy":"2023-05-02T10:11:20.197837Z","iopub.execute_input":"2023-05-02T10:11:20.198180Z","iopub.status.idle":"2023-05-02T10:11:20.940674Z","shell.execute_reply.started":"2023-05-02T10:11:20.198148Z","shell.execute_reply":"2023-05-02T10:11:20.939206Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"code","source":"show_box(1390)","metadata":{"_uuid":"a572542ea8f81af85b49ed8b56eb9f9e86535e41","execution":{"iopub.status.busy":"2023-05-02T10:11:20.942395Z","iopub.execute_input":"2023-05-02T10:11:20.942814Z","iopub.status.idle":"2023-05-02T10:11:21.695637Z","shell.execute_reply.started":"2023-05-02T10:11:20.942774Z","shell.execute_reply":"2023-05-02T10:11:21.694246Z"},"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"markdown","source":"The final CSV file is stored as a new dataset [here](https://www.kaggle.com/datasets/jeffaudi/airbus-ships-annotations-oriented-bounding-boxes). ","metadata":{"_uuid":"37a6d5607db35942335d4e66694ab01b0c6fd124"}},{"cell_type":"code","source":"","metadata":{"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"code","source":"","metadata":{"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"code","source":"","metadata":{"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"code","source":"","metadata":{"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"code","source":"","metadata":{"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"code","source":"","metadata":{"trusted":true},"execution_count":null,"outputs":[]},{"cell_type":"code","source":"","metadata":{},"execution_count":null,"outputs":[]}]}