Skip to content

Instantly share code, notes, and snippets.

Show Gist options
  • Save dusskapark/8adf27e4b8c6e7c392811d24547bf27e to your computer and use it in GitHub Desktop.
Save dusskapark/8adf27e4b8c6e7c392811d24547bf27e to your computer and use it in GitHub Desktop.
Model Maker Object Detection for RICO.ipynb
Display the source blob
Display the rendered blob
Raw
{
"cells": [
{
"cell_type": "markdown",
"metadata": {
"id": "view-in-github",
"colab_type": "text"
},
"source": [
"<a href=\"https://colab.research.google.com/gist/dusskapark/8adf27e4b8c6e7c392811d24547bf27e/model-maker-object-detection-for-rico.ipynb\" target=\"_parent\"><img src=\"https://colab.research.google.com/assets/colab-badge.svg\" alt=\"Open In Colab\"/></a>"
]
},
{
"cell_type": "markdown",
"metadata": {
"id": "gf2if_fGDaWc"
},
"source": [
"##### References \n",
"\n",
"> This colab notebook were made by modifying references below: \n",
"> - [Train a custom object detection model using your data](https://youtu.be/-ZyFYniGUsw)\n",
"> - [Model Maker Object Detection for Android Figurine](https://colab.research.google.com/github/khanhlvg/tflite_raspberry_pi/blob/main/object_detection/Train_custom_model_tutorial.ipynb)\n",
"> - [Object Detection with TensorFlow Lite Model Maker](https://www.tensorflow.org/lite/tutorials/model_maker_object_detection)\n"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {
"cellView": "form",
"id": "jrmj83afDJrv"
},
"outputs": [],
"source": [
"#@title Licensed under the Apache License, Version 2.0 (the \"License\");\n",
"# you may not use this file except in compliance with the License.\n",
"# You may obtain a copy of the License at\n",
"#\n",
"# https://www.apache.org/licenses/LICENSE-2.0\n",
"#\n",
"# Unless required by applicable law or agreed to in writing, software\n",
"# distributed under the License is distributed on an \"AS IS\" BASIS,\n",
"# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n",
"# See the License for the specific language governing permissions and\n",
"# limitations under the License."
]
},
{
"cell_type": "markdown",
"metadata": {
"id": "PpJEzDG6DK2Q"
},
"source": [
"# Train a custom object detection model with TensorFlow Lite Model Maker\n",
"\n",
"In this colab notebook, you'll learn how to use the [TensorFlow Lite Model Maker](https://www.tensorflow.org/lite/guide/model_maker) to train a custom object detection model to detect UI objects and how to convert the TF lite model with TFJS.\n",
"\n",
"The Model Maker library uses *transfer learning* to simplify the process of training a TensorFlow Lite model using a custom dataset. Retraining a TensorFlow Lite model with your own custom dataset reduces the amount of training data required and will shorten the training time.\n"
]
},
{
"cell_type": "markdown",
"metadata": {
"id": "BRYjtwRZGBOI"
},
"source": [
"## Preparation\n",
"\n",
"### Install the required packages\n",
"Start by installing the required packages, including the Model Maker package from the [GitHub repo](https://github.com/tensorflow/examples/tree/master/tensorflow_examples/lite/model_maker) and the pycocotools library you'll use for evaluation."
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {
"colab": {
"base_uri": "https://localhost:8080/"
},
"id": "35BJmtVpAP_n",
"outputId": "7864c1ac-1760-4f3a-c8ea-a3f98e58b332"
},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"Note: you may need to restart the kernel to use updated packages.\n",
"Note: you may need to restart the kernel to use updated packages.\n",
"Note: you may need to restart the kernel to use updated packages.\n"
]
}
],
"source": [
"%pip install -q tflite-model-maker\n",
"%pip install -q tflite-support\n",
"%pip install -q pycocotools"
]
},
{
"cell_type": "markdown",
"metadata": {
"id": "prQ86DdtD317"
},
"source": [
"Import the required packages."
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {
"id": "l4QQTXHHATDS"
},
"outputs": [],
"source": [
"from absl import logging\n",
"import tensorflow as tf\n",
"from tflite_support import metadata\n",
"import numpy as np\n",
"import os\n",
"\n",
"from tflite_model_maker.config import ExportFormat, QuantizationConfig\n",
"from tflite_model_maker import model_spec\n",
"from tflite_model_maker import object_detector\n",
"\n",
"\n",
"assert tf.__version__.startswith('2')\n",
"\n",
"tf.get_logger().setLevel('ERROR')\n",
"logging.set_verbosity(logging.ERROR)\n"
]
},
{
"cell_type": "markdown",
"metadata": {
"id": "3g6aQvXsD78P"
},
"source": [
"### Prepare the dataset\n",
"\n",
"In this notebook, we're going to rearrange and use the RICO dataset. If you would like to manually build training and validation datasets, please click [this link](https://interactionmining.org/rico) and download RICO manually.\n",
"\n",
"#### Downloading data from RICO dataset"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {
"id": "8AGg7D4JAV62"
},
"outputs": [],
"source": [
"!curl -L \"https://storage.googleapis.com/crowdstf-rico-uiuc-4540/rico_dataset_v0.1/unique_uis.tar.gz\" > jpg.tar.gz; tar -zxvf jpg.tar.gz; rm jpg.tar.gz\n",
"!curl -L \"https://storage.googleapis.com/crowdstf-rico-uiuc-4540/rico_dataset_v0.1/semantic_annotations.zip\" > json.zip; unzip json.zip; rm json.zip"
]
},
{
"cell_type": "markdown",
"metadata": {
"id": "-6tDEWQSVtqb"
},
"source": [
"Download two compressed files from RICO. Each file contains pairs of images and JSON files. We save space by deleting all files except for the original image files and annotation files we need."
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {
"id": "yurpxo2uVwBl"
},
"outputs": [],
"source": [
"# remove unused pairs\n",
"!find combined/. -name '*.json' -type f -delete\n",
"!find combined/. -name \"*.jpg\" | wc -l\n",
"!find semantic_annotations/. -name '*.png' -type f -delete\n",
"!find semantic_annotations/. -name \"*.json\" | wc -l"
]
},
{
"cell_type": "markdown",
"metadata": {
"id": "JWD2NiCKVzDP"
},
"source": [
"#### Scaled images \n",
"\n",
"Most of RICO's screenshots are high-resolution images at 1440 × 2560 pixels. If you use these directly, they will use a lot of resources with regards to the GPU and memory within Google Colab's training environment. \n",
"\n",
"So we'll reduce all images to 640px height JPG. Later, we're going to change the value inside annotation files too.\n"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {
"id": "RDpic-LQVz6B"
},
"outputs": [],
"source": [
"from PIL import Image\n",
"import os\n",
"\n",
"raw_path = './combined/' # source image path\n",
"data_path = './jpg/' # Resized image path\n",
"\n",
"# Start resize --------------------\n",
"# If there is no data_path, create\n",
"if not os.path.exists(data_path):\n",
" os.mkdir(data_path)\n",
"\n",
"# Specify a list of all images in the source image path\n",
"data_list = os.listdir(raw_path)\n",
"print(len(data_list))\n",
"\n",
"# Save all images after resizing\n",
"for name in data_list:\n",
" im = Image.open(raw_path + name)\n",
" im = im.resize((360, 640))\n",
" im.save(data_path + name)\n",
" print('end ::: ' + data_path + name)\n"
]
},
{
"cell_type": "markdown",
"metadata": {
"id": "qnSNCdVpV7_B"
},
"source": [
"#### Convert to XML\n",
"\n",
"We'll extract only the necessary information such as such as the bounding box, filename, and component name from the JSON files and convert them to xml format.\n",
"\n",
"During this conversion, I discovered some data had unexpected errors:\n",
"\n",
"- Bounding box (bndbox) is sometimes negative\n",
"- Component’s xmax or ymax value is sometimes greater than the overall width and height values of the screenshot\n",
"- xmin is sometimes greater than xmax or ymin is greater than ymax\n",
"- And so on…\n",
"\n",
"So, I skipped all of those kinda errored items. Please check the isvalidbdnbox function below:"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {
"id": "A6P3not9XWU3"
},
"outputs": [],
"source": [
"import os\n",
"import json\n",
"from xml.etree.ElementTree import Element, SubElement\n",
"\n",
"def beautify(elem, indent=0):\n",
" \"\"\"\n",
" xml 트리를 문자열로 변환합니다.\n",
" Converts the XML tree to a string.\n",
"\n",
" :param elem: xml element\n",
" :param indent: Indent Level to display in front\n",
" \"\"\"\n",
" result0 = f\"{' ' * indent}<{elem.tag}>\"\n",
" # 값이 있는 태그면 값을 바로 출력\n",
" # If there is a value in the tag, immediately print the value\n",
" if elem.text is not None:\n",
" result0 += elem.text + f\"</{elem.tag}>\\n\"\n",
" # 값이 없고 자식 노드가 있으면 재귀 호출로 출력합니다.\n",
" # If the tag has no value and has child nodes, call the recursive function.\n",
" else:\n",
" result0 += \"\\n\"\n",
" for _child in elem:\n",
" result0 += beautify(_child, indent + 1)\n",
" result0 += f\"{' ' * indent}</{elem.tag}>\\n\"\n",
" return result0\n",
"\n",
"def isvalidbdnbox(xmin,xmax,ymin,ymax):\n",
" \"\"\"\n",
" Validate the bnd box.\n",
" 다중 if를 추가해서 bnd 박스를 검증해야 합니다.\n",
" \"\"\"\n",
" # bounding box(bndbox)가 음수이면 안됩니다. \n",
" # bounding box(bndbox) cannot be negative.\n",
" if(xmin<0 or xmax<0 or ymin<0 or ymax<0):\n",
" print(\"bndbox cannot be negative.\")\n",
" return False\n",
" # bndbox의 xmax 값은 1440, ymax 값은 2560을 넘으면 안됩니다. \n",
" # The xmax value of bndbox cannot exceed 1440 and the ymax value cannot exceed 2560.\n",
" if(xmax>1440 or ymax>2560):\n",
" print(\"bndbox cannot exceed the screenshot\")\n",
" return False\n",
"\n",
" # xmin 값은 xmax 보다 클 수 없습니다. 또는 ymin 값은 ymax 보다 클 수 없습니다. \n",
" # xmin cannot be greater than xmax or ymin cannot be greater than ymax.\n",
" \n",
" if(xmin>xmax or ymin>ymax):\n",
" print(\"xmin>xmax or ymin>ymax\")\n",
" return False\n",
"\n",
" if(xmin==xmax or ymin==ymax):\n",
" print(\"xmin=xmax or ymin=ymax\")\n",
" return False\n",
" \n",
" \n",
" return True\n",
"\n",
"def recursive(child, result_out):\n",
" \"\"\"\n",
" 원하는 역할을 하기 위해서 재귀호출을 할 수 있는 함수를 생성합니다.\n",
" Create a function that makes a recursive call.\n",
" \"\"\"\n",
" obj = Element(\"object\")\n",
"\n",
" # Set bounds\n",
" bounds = child['bounds']\n",
"\n",
" # Set name\n",
" SubElement(obj, \"name\").text = child['componentLabel']\n",
" # Set difficult, truncated, pose\n",
" SubElement(obj, \"difficult\").text = '0'\n",
" SubElement(obj, \"truncated\").text = 'Unspecified'\n",
" SubElement(obj, \"pose\").text = 'Undefined'\n",
"\n",
" # Set bndbox \n",
" bndbox = SubElement(obj, \"bndbox\")\n",
" xmin=bounds[0]\n",
" ymin=bounds[1]\n",
" xmax=bounds[2]\n",
" ymax=bounds[3]\n",
" \n",
" if(isvalidbdnbox(xmin,xmax,ymin,ymax)):\n",
" SubElement(bndbox, \"xmin\").text = str(round(xmin/4))\n",
" SubElement(bndbox, \"ymin\").text = str(round(ymin/4))\n",
" SubElement(bndbox, \"xmax\").text = str(round(xmax/4))\n",
" SubElement(bndbox, \"ymax\").text = str(round(ymax/4))\n",
" result_out.append(beautify(obj))\n",
"\n",
" \n",
" # 생성한 object 태그를 문자열로 변환해서 추가합니다.\n",
" # Convert the created object tag to a string and add it.\n",
" if 'children' not in child:\n",
" return\n",
"\n",
" # 자식 노드가 있는 경우 자식 노드에 대해 재귀 호출을 수행합니다.\n",
" # If there is a child node, make a recursive call to the child node.\n",
" for ch in child.get('children', []):\n",
" recursive(ch, result_out)\n",
"\n",
"\n",
"def json2xml(infile, outfile):\n",
" \"\"\"\n",
" json2xml function\n",
" param infile :\n",
" ourfile :\n",
" \"\"\"\n",
" result_out = []\n",
" imgName = infile.replace(\"./semantic_annotations/\", \"\")\n",
" imgName = imgName.replace(\"json\", \"jpg\")\n",
"\n",
" # Read the file\n",
" with open(infile, \"r\", encoding=\"UTF-8\") as f:\n",
" data = json.load(f)\n",
"\n",
" children = data['children']\n",
"\n",
" # 자식 노드에 대해 재귀 호출을 수행합니다.\n",
" # Make a recursive call on child nodes.\n",
" for child in children:\n",
" recursive(child, result_out)\n",
"\n",
" # 그리고 해당 결과를 파일로 저장합니다.\n",
" # And save the result to the XML file.\n",
" with open(outfile, \"w\", encoding=\"UTF-8\") as f:\n",
" f.write(\"\".join(\"<annotation><folder />\"))\n",
" f.write(\"\".join(\"<filename>\" + imgName + \"</filename>\\n\"\n",
" \"<path>\" + imgName + \"</path>\\n\"+\n",
" \"<source><database>RICO</database></source><size><width>360</width><height>640</height><depth>3</depth></size><segmented>0</segmented> \"))\n",
" f.write(\"\".join(result_out))\n",
" f.write(\"\".join(\"</annotation>\"))\n",
"\n",
"\n",
"def search(mypath):\n",
" onlyfiles = [f for f in os.listdir(mypath)\n",
" if os.path.isfile(os.path.join(mypath, f))]\n",
" onlyfiles.sort() \n",
" return onlyfiles\n",
"\n",
"\n",
"def main():\n",
" \"\"\"\n",
" 메인 함수\n",
" The main function \n",
" \"\"\"\n",
" data_path = './xml/'\n",
" if not os.path.exists(data_path):\n",
" os.mkdir(data_path)\n",
"\n",
"\n",
" files = os.listdir('./semantic_annotations')\n",
" # print(files)\n",
" for infile in files:\n",
" infile = f'./semantic_annotations/{infile}'\n",
" outfile = infile.replace(\"semantic_annotations\", \"xml\").replace(\"json\", \"xml\")\n",
" print(infile, outfile)\n",
" json2xml(infile, outfile)\n",
"\n",
"\n",
"if __name__ == \"__main__\":\n",
" main()"
]
},
{
"cell_type": "markdown",
"metadata": {
"id": "5dXpiFSoV_8f"
},
"source": [
"#### Generate Label Map\n",
"\n",
"Next, we should generate the `label_map.pbtxt` based on XML file and also set the `label_map` array together.\n"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {
"colab": {
"base_uri": "https://localhost:8080/"
},
"id": "Sg8P9fdBWCzj",
"outputId": "95484b7f-2566-4c91-eaf1-6e046bb65b4d"
},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"['Image', 'Input', 'On/Off Switch', 'Date Picker', 'Map View', 'List Item', 'Advertisement', 'Card', 'Checkbox', 'Drawer', 'Web View', 'Radio Button', 'Video', 'Button Bar', 'Text', 'Bottom Navigation', 'Toolbar', 'Number Stepper', 'Text Button', 'Pager Indicator', 'Icon', 'Slider', 'Modal', 'Background Image', 'Multi-Tab']\n"
]
}
],
"source": [
"import os\n",
"import xml.etree.ElementTree as ET\n",
"\n",
"obj = []\n",
"\n",
"for filename in os.listdir(\"./xml/\"):\n",
" # with open(os.path.join(\"xml\", filename), 'r') as f:\n",
" tree = ET.parse(os.path.join(\"./xml/\", filename))\n",
" root = tree.getroot()\n",
" object = root.findall(\"object\")\n",
" name = [x.findtext(\"name\") for x in object]\n",
"\n",
" for i in name:\n",
" obj.append(i)\n",
"\n",
"obj_unique = list(set(obj))\n",
"\n",
"pbtxt = \"\"\n",
"label_map = []\n",
"for i in range(len(obj_unique)):\n",
" pbtxt += \"item {\\n name: \\\"\"+obj_unique[i]+\"\\\",\\n id: \"+str(i+1)+\"\\n}\\n\"+\"\\n\"\n",
" label_map.append(obj_unique[i])\n",
" \n",
"with open(\"label_map.pbtxt\", \"w\", encoding=\"utf-8\") as f:\n",
" f.write(pbtxt)\n",
"\n",
"print(label_map)"
]
},
{
"cell_type": "markdown",
"metadata": {
"id": "T9hbPvymZofa"
},
"source": [
"#### Partition the dataset\n",
"\n",
"Next, We're going to split our dataset into the desired training and testing subsets. Typically, the ratio is 9:1. 90% of the images are used for training and the rest 10% is maintained for testing, but you can chose whatever ratio suits your needs.\n",
"\n",
"[Lyudmil Vladimirov](https://github.com/sglvladi) has published a great code example on [splitting the dataset](https://tensorflow-object-detection-api-tutorial.readthedocs.io/en/latest/training.html#partition-the-dataset)."
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {
"id": "jaEobd0sZrBe"
},
"outputs": [],
"source": [
"import os\n",
"import re\n",
"from shutil import copyfile\n",
"import argparse\n",
"import math\n",
"import random\n",
"\n",
"\n",
"def iterate_dir(source, dest, ratio, xml_source):\n",
" source = source.replace('\\\\', '/')\n",
" xml_source = xml_source.replace('\\\\', '/')\n",
" dest = dest.replace('\\\\', '/')\n",
" train_dir = os.path.join(dest, 'train')\n",
" test_dir = os.path.join(dest, 'test')\n",
"\n",
" if not os.path.exists(train_dir):\n",
" os.makedirs(train_dir)\n",
" if not os.path.exists(test_dir):\n",
" os.makedirs(test_dir)\n",
"\n",
" images = [f for f in os.listdir(source)\n",
" if re.search(r'([a-zA-Z0-9\\s_\\\\.\\-\\(\\):])+(?i)(.jpg|.jpeg|.png)$', f)]\n",
"\n",
" num_images = len(images)\n",
" num_test_images = math.ceil(ratio*num_images)\n",
"\n",
" for i in range(num_test_images):\n",
" idx = random.randint(0, len(images)-1)\n",
" filename = images[idx]\n",
" copyfile(os.path.join(source, filename),os.path.join(test_dir, filename))\n",
" \n",
" xml_filename = os.path.splitext(filename)[0]+'.xml'\n",
" copyfile(os.path.join(xml_source, xml_filename),os.path.join(test_dir, xml_filename))\n",
" images.remove(images[idx])\n",
"\n",
" for filename in images:\n",
" copyfile(os.path.join(source, filename),os.path.join(train_dir, filename))\n",
" \n",
" xml_filename = os.path.splitext(filename)[0]+'.xml'\n",
" copyfile(os.path.join(xml_source, xml_filename),os.path.join(train_dir, xml_filename))\n",
" \n",
"def main():\n",
"\n",
" imageDir = './jpg/'\n",
" outputDir = './'\n",
" ratio = 0.1\n",
" xmlDir = './xml/'\n",
"\n",
" # Now we are ready to start the iteration\n",
" iterate_dir(imageDir, outputDir, ratio, xmlDir)\n",
"\n",
"\n",
"if __name__ == '__main__':\n",
" main()"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {
"colab": {
"base_uri": "https://localhost:8080/"
},
"id": "XYvfmDYRdm3_",
"outputId": "4a436c7b-c34d-4a4c-e4e3-b7e2a9ea0799"
},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"119269\n",
"13255\n"
]
},
{
"name": "stderr",
"output_type": "stream",
"text": [
"UsageError: Line magic function `%rm` not found.\n"
]
}
],
"source": [
"# (optional) Leave only the necessary files.\n",
"\n",
"!find train/. | wc -l\n",
"!find test/. | wc -l\n",
"\n",
"%rm -rf semantic_annotations combined jpg\n",
"# %rm -rf test train"
]
},
{
"cell_type": "markdown",
"metadata": {
"id": "Yxh3KInCFeB-"
},
"source": [
"## Train the object detection model\n",
"\n",
"### Step 1: Load the dataset\n",
"\n",
"* Images in `train_data` is used to train the custom object detection model.\n",
"* Images in `val_data` is used to check if the model can generalize well to new images that it hasn't seen before."
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {
"id": "p0UAF9Qqpu5e"
},
"outputs": [],
"source": [
"# print(label_map)\n",
"train_data = object_detector.DataLoader.from_pascal_voc(\n",
" images_dir='./train',\n",
" annotations_dir='./train',\n",
" label_map=label_map\n",
")\n",
"\n",
"val_data = object_detector.DataLoader.from_pascal_voc(\n",
" images_dir='./test',\n",
" annotations_dir='./test',\n",
" label_map=label_map\n",
")\n"
]
},
{
"cell_type": "markdown",
"metadata": {
"id": "UNRhB8N7GHXj"
},
"source": [
"### Step 2: Select a model architecture\n",
"\n",
"EfficientDet-Lite[0-4] are a family of mobile/IoT-friendly object detection models derived from the [EfficientDet](https://arxiv.org/abs/1911.09070) architecture.\n",
"\n",
"Here is the performance of each EfficientDet-Lite models compared to each others.\n",
"\n",
"| Model architecture | Size(MB)* |Latency(ms)**\t| Average Precision*** |\n",
"|--------------------|-----------|--------------|----------------------|\n",
"| EfficientDet-Lite0 | 4.4 | \t37 | 25.69% |\n",
"| EfficientDet-Lite1 | 5.8 | 49 | 30.55% |\n",
"| EfficientDet-Lite2 | 7.2 | 69 | 33.97% |\n",
"| EfficientDet-Lite3 | 11.4 | 116 | 37.70% |\n",
"| EfficientDet-Lite4 | 19.9 | 260 | 41.96% |\n",
"\n",
"<i> * Size of the integer quantized models. <br/>\n",
"** Latency measured on Pixel 4 using 4 threads on CPU.<br/>\n",
"*** Average Precision is the mAP (mean Average Precision) on the COCO 2017 validation dataset.\n",
"</i>\n",
"\n",
"In this notebook, we use EfficientDet-Lite0 to train our model. You can choose other model architectures depending on whether speed or accuracy is more important to you. \n",
"\n",
"**note:** You might need to pass the max instance number as an hparam when the model is created due to huge volume of RICO dataset. Set the `max_instances_per_image` slightly higher than the max number of objects you expect to see in an image."
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {
"id": "GZOojrDHAY1J"
},
"outputs": [],
"source": [
"spec = object_detector.EfficientDetSpec(model_name='efficientdet-lite0', uri='https://tfhub.dev/tensorflow/efficientdet/lite0/feature-vector/1', hparams={'max_instances_per_image': 8000})"
]
},
{
"cell_type": "markdown",
"metadata": {
"id": "5aeDU4mIM4ft"
},
"source": [
"### Step 3: Train the TensorFlow model with the training data.\n",
"\n",
"* Set `epochs = 20`, which means it will go through the training dataset 20 times. You can look at the validation accuracy during training and stop when you see validation loss (`val_loss`) stop decreasing to avoid overfitting.\n",
"* Set `batch_size = 16` here so you will see that it takes 3,727 steps to go through the all screenshots in the training dataset.\n",
"* Set `train_whole_model=True` to fine-tune the whole model instead of just training the head layer to improve accuracy. The trade-off is that it may take longer to train the model."
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {
"colab": {
"base_uri": "https://localhost:8080/",
"height": 471
},
"id": "_MClfpsJAfda",
"outputId": "3ff302ef-20a6-400b-970a-f159e8214e34"
},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"Epoch 1/20\n",
"3727/3727 [==============================] - 17525s 5s/step - det_loss: 1.0959 - cls_loss: 0.6612 - box_loss: 0.0087 - reg_l2_loss: 0.0672 - loss: 1.1631 - learning_rate: 0.0140 - gradient_norm: 1.4641 - val_det_loss: 1.0653 - val_cls_loss: 0.6432 - val_box_loss: 0.0084 - val_reg_l2_loss: 0.0673 - val_loss: 1.1326\n",
"Epoch 2/20\n",
"3727/3727 [==============================] - 17482s 5s/step - det_loss: 0.9268 - cls_loss: 0.5615 - box_loss: 0.0073 - reg_l2_loss: 0.0674 - loss: 0.9942 - learning_rate: 0.0197 - gradient_norm: 1.2754 - val_det_loss: 0.9822 - val_cls_loss: 0.5799 - val_box_loss: 0.0080 - val_reg_l2_loss: 0.0675 - val_loss: 1.0497\n",
"Epoch 3/20\n",
"3727/3727 [==============================] - 17462s 5s/step - det_loss: 0.8908 - cls_loss: 0.5412 - box_loss: 0.0070 - reg_l2_loss: 0.0675 - loss: 0.9582 - learning_rate: 0.0191 - gradient_norm: 1.2383 - val_det_loss: 0.9547 - val_cls_loss: 0.5637 - val_box_loss: 0.0078 - val_reg_l2_loss: 0.0674 - val_loss: 1.0221\n",
"Epoch 4/20\n",
"3727/3727 [==============================] - 17494s 5s/step - det_loss: 0.8632 - cls_loss: 0.5253 - box_loss: 0.0068 - reg_l2_loss: 0.0673 - loss: 0.9305 - learning_rate: 0.0184 - gradient_norm: 1.2212 - val_det_loss: 0.9452 - val_cls_loss: 0.5576 - val_box_loss: 0.0078 - val_reg_l2_loss: 0.0672 - val_loss: 1.0124\n",
"Epoch 5/20\n",
"3727/3727 [==============================] - 18025s 5s/step - det_loss: 0.8442 - cls_loss: 0.5165 - box_loss: 0.0066 - reg_l2_loss: 0.0670 - loss: 0.9112 - learning_rate: 0.0173 - gradient_norm: 1.2118 - val_det_loss: 0.8487 - val_cls_loss: 0.5037 - val_box_loss: 0.0069 - val_reg_l2_loss: 0.0668 - val_loss: 0.9155\n",
"Epoch 6/20\n",
"3727/3727 [==============================] - 17874s 5s/step - det_loss: 0.8329 - cls_loss: 0.5094 - box_loss: 0.0065 - reg_l2_loss: 0.0666 - loss: 0.8995 - learning_rate: 0.0161 - gradient_norm: 1.2254 - val_det_loss: 0.9447 - val_cls_loss: 0.5774 - val_box_loss: 0.0073 - val_reg_l2_loss: 0.0663 - val_loss: 1.0111\n",
"Epoch 7/20\n",
"3727/3727 [==============================] - 17848s 5s/step - det_loss: 0.8182 - cls_loss: 0.5023 - box_loss: 0.0063 - reg_l2_loss: 0.0661 - loss: 0.8844 - learning_rate: 0.0148 - gradient_norm: 1.2439 - val_det_loss: 0.9011 - val_cls_loss: 0.5337 - val_box_loss: 0.0073 - val_reg_l2_loss: 0.0659 - val_loss: 0.9670\n",
"Epoch 8/20\n",
"3727/3727 [==============================] - 17838s 5s/step - det_loss: 0.8099 - cls_loss: 0.4967 - box_loss: 0.0063 - reg_l2_loss: 0.0656 - loss: 0.8755 - learning_rate: 0.0132 - gradient_norm: 1.2692 - val_det_loss: 0.9309 - val_cls_loss: 0.5665 - val_box_loss: 0.0073 - val_reg_l2_loss: 0.0653 - val_loss: 0.9963\n",
"Epoch 9/20\n",
"3727/3727 [==============================] - 17868s 5s/step - det_loss: 0.7989 - cls_loss: 0.4908 - box_loss: 0.0062 - reg_l2_loss: 0.0651 - loss: 0.8640 - learning_rate: 0.0116 - gradient_norm: 1.2945 - val_det_loss: 0.9429 - val_cls_loss: 0.5695 - val_box_loss: 0.0075 - val_reg_l2_loss: 0.0648 - val_loss: 1.0077\n",
"Epoch 10/20\n",
"3727/3727 [==============================] - 18372s 5s/step - det_loss: 0.7906 - cls_loss: 0.4854 - box_loss: 0.0061 - reg_l2_loss: 0.0645 - loss: 0.8550 - learning_rate: 0.0100 - gradient_norm: 1.3375 - val_det_loss: 0.9003 - val_cls_loss: 0.5316 - val_box_loss: 0.0074 - val_reg_l2_loss: 0.0642 - val_loss: 0.9645\n",
"Epoch 11/20\n",
"3727/3727 [==============================] - 18466s 5s/step - det_loss: 0.7789 - cls_loss: 0.4792 - box_loss: 0.0060 - reg_l2_loss: 0.0639 - loss: 0.8428 - learning_rate: 0.0084 - gradient_norm: 1.3638 - val_det_loss: 0.8097 - val_cls_loss: 0.4916 - val_box_loss: 0.0064 - val_reg_l2_loss: 0.0636 - val_loss: 0.8734\n",
"Epoch 12/20\n",
"3727/3727 [==============================] - 18475s 5s/step - det_loss: 0.7732 - cls_loss: 0.4761 - box_loss: 0.0059 - reg_l2_loss: 0.0633 - loss: 0.8366 - learning_rate: 0.0068 - gradient_norm: 1.4166 - val_det_loss: 0.8956 - val_cls_loss: 0.5397 - val_box_loss: 0.0071 - val_reg_l2_loss: 0.0631 - val_loss: 0.9587\n",
"Epoch 13/20\n",
"3727/3727 [==============================] - 18503s 5s/step - det_loss: 0.7630 - cls_loss: 0.4708 - box_loss: 0.0058 - reg_l2_loss: 0.0628 - loss: 0.8258 - learning_rate: 0.0052 - gradient_norm: 1.4611 - val_det_loss: 0.9174 - val_cls_loss: 0.5333 - val_box_loss: 0.0077 - val_reg_l2_loss: 0.0626 - val_loss: 0.9800\n",
"Epoch 14/20\n",
"3727/3727 [==============================] - 18504s 5s/step - det_loss: 0.7548 - cls_loss: 0.4654 - box_loss: 0.0058 - reg_l2_loss: 0.0623 - loss: 0.8171 - learning_rate: 0.0039 - gradient_norm: 1.5110 - val_det_loss: 0.8530 - val_cls_loss: 0.5123 - val_box_loss: 0.0068 - val_reg_l2_loss: 0.0621 - val_loss: 0.9151\n",
"Epoch 15/20\n",
"3727/3727 [==============================] - 18990s 5s/step - det_loss: 0.7483 - cls_loss: 0.4624 - box_loss: 0.0057 - reg_l2_loss: 0.0620 - loss: 0.8103 - learning_rate: 0.0027 - gradient_norm: 1.5508 - val_det_loss: 0.7532 - val_cls_loss: 0.4612 - val_box_loss: 0.0058 - val_reg_l2_loss: 0.0618 - val_loss: 0.8150\n",
"Epoch 16/20\n",
"3727/3727 [==============================] - 18562s 5s/step - det_loss: 0.7427 - cls_loss: 0.4586 - box_loss: 0.0057 - reg_l2_loss: 0.0617 - loss: 0.8044 - learning_rate: 0.0016 - gradient_norm: 1.5845 - val_det_loss: 0.7677 - val_cls_loss: 0.4650 - val_box_loss: 0.0061 - val_reg_l2_loss: 0.0616 - val_loss: 0.8293\n",
"Epoch 17/20\n",
"3727/3727 [==============================] - 18550s 5s/step - det_loss: 0.7350 - cls_loss: 0.4541 - box_loss: 0.0056 - reg_l2_loss: 0.0615 - loss: 0.7965 - learning_rate: 8.5270e-04 - gradient_norm: 1.6145 - val_det_loss: 0.7575 - val_cls_loss: 0.4621 - val_box_loss: 0.0059 - val_reg_l2_loss: 0.0614 - val_loss: 0.8190\n",
"Epoch 18/20\n",
"3727/3727 [==============================] - 18554s 5s/step - det_loss: 0.7340 - cls_loss: 0.4538 - box_loss: 0.0056 - reg_l2_loss: 0.0614 - loss: 0.7954 - learning_rate: 3.1704e-04 - gradient_norm: 1.6074 - val_det_loss: 0.7528 - val_cls_loss: 0.4576 - val_box_loss: 0.0059 - val_reg_l2_loss: 0.0614 - val_loss: 0.8141\n",
"Epoch 19/20\n",
"3727/3727 [==============================] - 17843s 5s/step - det_loss: 0.7312 - cls_loss: 0.4524 - box_loss: 0.0056 - reg_l2_loss: 0.0614 - loss: 0.7926 - learning_rate: 4.5510e-05 - gradient_norm: 1.6218 - val_det_loss: 0.7502 - val_cls_loss: 0.4556 - val_box_loss: 0.0059 - val_reg_l2_loss: 0.0614 - val_loss: 0.8116\n",
"Epoch 20/20\n",
"3727/3727 [==============================] - 16614s 4s/step - det_loss: 0.7300 - cls_loss: 0.4511 - box_loss: 0.0056 - reg_l2_loss: 0.0614 - loss: 0.7913 - learning_rate: 4.5510e-05 - gradient_norm: 1.6283 - val_det_loss: 0.7491 - val_cls_loss: 0.4552 - val_box_loss: 0.0059 - val_reg_l2_loss: 0.0614 - val_loss: 0.8105\n"
]
}
],
"source": [
"model = object_detector.create(train_data=train_data, model_spec=spec, batch_size=16, train_whole_model=True, epochs=20, validation_data=val_data)"
]
},
{
"cell_type": "markdown",
"metadata": {
"id": "KB4hKeerMmh4"
},
"source": [
"### Step 4. Evaluate the model with the validation data.\n",
"\n",
"After training the object detection model using the images in the training dataset, use the screenshots in the validation dataset to evaluate how the model performs against new data it has never seen before.\n",
"\n",
"As the default batch size is 64, it will take 1 step to go through the screenshots in the validation dataset.\n",
"\n",
"The evaluation metrics are same as [COCO](https://cocodataset.org/#detection-eval)."
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {
"colab": {
"base_uri": "https://localhost:8080/"
},
"id": "OUqEpcYwAg8L",
"outputId": "b359dc94-4906-4b5b-e022-77030b7a122f"
},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"104/104 [==============================] - 442s 4s/step\n",
"\n"
]
},
{
"data": {
"text/plain": [
"{'AP': 0.23928128,\n",
" 'AP50': 0.34784698,\n",
" 'AP75': 0.26955733,\n",
" 'APs': 0.028244007,\n",
" 'APm': 0.12941888,\n",
" 'APl': 0.22108501,\n",
" 'ARmax1': 0.2461824,\n",
" 'ARmax10': 0.35139942,\n",
" 'ARmax100': 0.36341742,\n",
" 'ARs': 0.054406602,\n",
" 'ARm': 0.19773099,\n",
" 'ARl': 0.34407103,\n",
" 'AP_/Image': 0.16317038,\n",
" 'AP_/Input': 0.08524754,\n",
" 'AP_/On/Off Switch': 0.048255134,\n",
" 'AP_/Date Picker': 0.35675347,\n",
" 'AP_/Map View': 0.17753105,\n",
" 'AP_/List Item': 0.28585535,\n",
" 'AP_/Advertisement': 0.28919548,\n",
" 'AP_/Card': 0.20789924,\n",
" 'AP_/Checkbox': 0.1734771,\n",
" 'AP_/Drawer': 0.68573767,\n",
" 'AP_/Web View': 0.19177885,\n",
" 'AP_/Radio Button': 0.057744134,\n",
" 'AP_/Video': 0.0060326965,\n",
" 'AP_/Button Bar': 0.0023122337,\n",
" 'AP_/Text': 0.105855204,\n",
" 'AP_/Bottom Navigation': 0.3663874,\n",
" 'AP_/Toolbar': 0.5422325,\n",
" 'AP_/Number Stepper': 0.35072106,\n",
" 'AP_/Text Button': 0.2381703,\n",
" 'AP_/Pager Indicator': 0.000990099,\n",
" 'AP_/Icon': 0.34229085,\n",
" 'AP_/Slider': 0.0,\n",
" 'AP_/Modal': 0.63886,\n",
" 'AP_/Background Image': 0.46593183,\n",
" 'AP_/Multi-Tab': 0.19960245}"
]
},
"execution_count": 7,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"model.evaluate(val_data)"
]
},
{
"cell_type": "markdown",
"metadata": {
"id": "NARVYk9rGLIl"
},
"source": [
"### Step 5: Export as a TensorFlow Lite model with TFJS format.\n",
"\n",
"Export the trained object detection model to the TensorFlow JS format by specifying which folder you want to export the quantized model to. The default post-training quantization technique is [full integer quantization](https://www.tensorflow.org/lite/performance/post_training_integer_quant). "
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {
"id": "_u3eFxoBAiqE",
"outputId": "f3fa26d4-fdbe-4ecf-d5a0-f39b76f75032"
},
"outputs": [
{
"name": "stderr",
"output_type": "stream",
"text": [
"'zip'��(��) ���� �Ǵ� �ܺ� ����, ������ �� �ִ� ���α׷�, �Ǵ�\n",
"��ġ ������ �ƴմϴ�.\n"
]
}
],
"source": [
"# ExportFormat.TFJS is not yet supported\n",
"# model.export(export_dir=\"./js_export/\", export_format=[ExportFormat.TFJS])\n",
"\n",
"model.export(export_dir=\"./js_export/\", export_format=[ExportFormat.SAVED_MODEL])\n",
"!zip -r ./js_export/ModelFiles.zip ./js_export/"
]
},
{
"cell_type": "markdown",
"metadata": {
"id": "wnqktl45PZRy"
},
"source": [
"## (Optional) Test the detection model with TensorFlow lite\n",
"\n",
"Let's test it with an image that the model hasn't seen before to get a sense of how good the model is.\n",
"\n",
"In this example, we will extract one more tflite model and test how well the model actually works. This result may be slightly different from that of TFJS, so it is recommended to use it for reference only."
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {
"id": "v2h_pF2-osg7",
"outputId": "45d12473-41ee-46d4-cbff-0d5d2c3f7a51"
},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"6627/6627 [==============================] - 27236s 4s/step\n",
"\n"
]
},
{
"data": {
"text/plain": [
"{'AP': 0.20490232,\n",
" 'AP50': 0.2911546,\n",
" 'AP75': 0.23508495,\n",
" 'APs': 0.021943321,\n",
" 'APm': 0.115160994,\n",
" 'APl': 0.18580821,\n",
" 'ARmax1': 0.2026951,\n",
" 'ARmax10': 0.26787397,\n",
" 'ARmax100': 0.26974252,\n",
" 'ARs': 0.032209698,\n",
" 'ARm': 0.15022126,\n",
" 'ARl': 0.24558628,\n",
" 'AP_/Image': 0.123515174,\n",
" 'AP_/Input': 0.07007123,\n",
" 'AP_/On/Off Switch': 0.045216367,\n",
" 'AP_/Date Picker': 0.22942775,\n",
" 'AP_/Map View': 0.1260201,\n",
" 'AP_/List Item': 0.22389112,\n",
" 'AP_/Advertisement': 0.23017684,\n",
" 'AP_/Card': 0.11379653,\n",
" 'AP_/Checkbox': 0.14040026,\n",
" 'AP_/Drawer': 0.65390044,\n",
" 'AP_/Web View': 0.14987761,\n",
" 'AP_/Radio Button': 0.03745377,\n",
" 'AP_/Video': 0.026732674,\n",
" 'AP_/Button Bar': 0.0,\n",
" 'AP_/Text': 0.07595385,\n",
" 'AP_/Bottom Navigation': 0.38563696,\n",
" 'AP_/Toolbar': 0.5175326,\n",
" 'AP_/Number Stepper': 0.3151305,\n",
" 'AP_/Text Button': 0.21079078,\n",
" 'AP_/Pager Indicator': 0.0014851486,\n",
" 'AP_/Icon': 0.3101346,\n",
" 'AP_/Slider': 0.0,\n",
" 'AP_/Modal': 0.5949396,\n",
" 'AP_/Background Image': 0.41141692,\n",
" 'AP_/Multi-Tab': 0.12905711}"
]
},
"execution_count": 9,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"model.export(export_dir='.', tflite_filename='rico.tflite')\n",
"model.evaluate_tflite('rico.tflite', val_data)"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {
"cellView": "form",
"id": "9ZsLQtJ1AlW_"
},
"outputs": [],
"source": [
"#@title Load the trained TFLite model and define some visualization functions\n",
"\n",
"#@markdown This code comes from the TFLite Object Detection [Raspberry Pi sample](https://github.com/tensorflow/examples/tree/master/lite/examples/object_detection/raspberry_pi).\n",
"\n",
"import platform\n",
"from typing import List, NamedTuple\n",
"import json\n",
"\n",
"import cv2\n",
"\n",
"Interpreter = tf.lite.Interpreter\n",
"load_delegate = tf.lite.experimental.load_delegate\n",
"\n",
"# pylint: enable=g-import-not-at-top\n",
"\n",
"\n",
"class ObjectDetectorOptions(NamedTuple):\n",
" \"\"\"A config to initialize an object detector.\"\"\"\n",
"\n",
" enable_edgetpu: bool = False\n",
" \"\"\"Enable the model to run on EdgeTPU.\"\"\"\n",
"\n",
" label_allow_list: List[str] = None\n",
" \"\"\"The optional allow list of labels.\"\"\"\n",
"\n",
" label_deny_list: List[str] = None\n",
" \"\"\"The optional deny list of labels.\"\"\"\n",
"\n",
" max_results: int = -1\n",
" \"\"\"The maximum number of top-scored detection results to return.\"\"\"\n",
"\n",
" num_threads: int = 1\n",
" \"\"\"The number of CPU threads to be used.\"\"\"\n",
"\n",
" score_threshold: float = 0.0\n",
" \"\"\"The score threshold of detection results to return.\"\"\"\n",
"\n",
"\n",
"class Rect(NamedTuple):\n",
" \"\"\"A rectangle in 2D space.\"\"\"\n",
" left: float\n",
" top: float\n",
" right: float\n",
" bottom: float\n",
"\n",
"\n",
"class Category(NamedTuple):\n",
" \"\"\"A result of a classification task.\"\"\"\n",
" label: str\n",
" score: float\n",
" index: int\n",
"\n",
"\n",
"class Detection(NamedTuple):\n",
" \"\"\"A detected object as the result of an ObjectDetector.\"\"\"\n",
" bounding_box: Rect\n",
" categories: List[Category]\n",
"\n",
"\n",
"def edgetpu_lib_name():\n",
" \"\"\"Returns the library name of EdgeTPU in the current platform.\"\"\"\n",
" return {\n",
" 'Darwin': 'libedgetpu.1.dylib',\n",
" 'Linux': 'libedgetpu.so.1',\n",
" 'Windows': 'edgetpu.dll',\n",
" }.get(platform.system(), None)\n",
"\n",
"\n",
"class ObjectDetector:\n",
" \"\"\"A wrapper class for a TFLite object detection model.\"\"\"\n",
"\n",
" _OUTPUT_LOCATION_NAME = 'location'\n",
" _OUTPUT_CATEGORY_NAME = 'category'\n",
" _OUTPUT_SCORE_NAME = 'score'\n",
" _OUTPUT_NUMBER_NAME = 'number of detections'\n",
"\n",
" def __init__(\n",
" self,\n",
" model_path: str,\n",
" options: ObjectDetectorOptions = ObjectDetectorOptions()\n",
" ) -> None:\n",
" \"\"\"Initialize a TFLite object detection model.\n",
" Args:\n",
" model_path: Path to the TFLite model.\n",
" options: The config to initialize an object detector. (Optional)\n",
" Raises:\n",
" ValueError: If the TFLite model is invalid.\n",
" OSError: If the current OS isn't supported by EdgeTPU.\n",
" \"\"\"\n",
"\n",
" # Load metadata from model.\n",
" displayer = metadata.MetadataDisplayer.with_model_file(model_path)\n",
"\n",
" # Save model metadata for preprocessing later.\n",
" model_metadata = json.loads(displayer.get_metadata_json())\n",
" process_units = model_metadata['subgraph_metadata'][0]['input_tensor_metadata'][0]['process_units']\n",
" mean = 0.0\n",
" std = 1.0\n",
" for option in process_units:\n",
" if option['options_type'] == 'NormalizationOptions':\n",
" mean = option['options']['mean'][0]\n",
" std = option['options']['std'][0]\n",
" self._mean = mean\n",
" self._std = std\n",
"\n",
" # Load label list from metadata.\n",
" file_name = displayer.get_packed_associated_file_list()[0]\n",
" label_map_file = displayer.get_associated_file_buffer(file_name).decode()\n",
" label_list = list(filter(lambda x: len(x) > 0, label_map_file.splitlines()))\n",
" self._label_list = label_list\n",
"\n",
" # Initialize TFLite model.\n",
" if options.enable_edgetpu:\n",
" if edgetpu_lib_name() is None:\n",
" raise OSError(\"The current OS isn't supported by Coral EdgeTPU.\")\n",
" interpreter = Interpreter(\n",
" model_path=model_path,\n",
" experimental_delegates=[load_delegate(edgetpu_lib_name())],\n",
" num_threads=options.num_threads)\n",
" else:\n",
" interpreter = Interpreter(\n",
" model_path=model_path, num_threads=options.num_threads)\n",
"\n",
" interpreter.allocate_tensors()\n",
" input_detail = interpreter.get_input_details()[0]\n",
"\n",
" # From TensorFlow 2.6, the order of the outputs become undefined.\n",
" # Therefore we need to sort the tensor indices of TFLite outputs and to know\n",
" # exactly the meaning of each output tensor. For example, if\n",
" # output indices are [601, 599, 598, 600], tensor names and indices aligned\n",
" # are:\n",
" # - location: 598\n",
" # - category: 599\n",
" # - score: 600\n",
" # - detection_count: 601\n",
" # because of the op's ports of TFLITE_DETECTION_POST_PROCESS\n",
" # (https://github.com/tensorflow/tensorflow/blob/a4fe268ea084e7d323133ed7b986e0ae259a2bc7/tensorflow/lite/kernels/detection_postprocess.cc#L47-L50).\n",
" sorted_output_indices = sorted(\n",
" [output['index'] for output in interpreter.get_output_details()])\n",
" self._output_indices = {\n",
" self._OUTPUT_LOCATION_NAME: sorted_output_indices[0],\n",
" self._OUTPUT_CATEGORY_NAME: sorted_output_indices[1],\n",
" self._OUTPUT_SCORE_NAME: sorted_output_indices[2],\n",
" self._OUTPUT_NUMBER_NAME: sorted_output_indices[3],\n",
" }\n",
"\n",
" self._input_size = input_detail['shape'][2], input_detail['shape'][1]\n",
" self._is_quantized_input = input_detail['dtype'] == np.uint8\n",
" self._interpreter = interpreter\n",
" self._options = options\n",
"\n",
" def detect(self, input_image: np.ndarray) -> List[Detection]:\n",
" \"\"\"Run detection on an input image.\n",
" Args:\n",
" input_image: A [height, width, 3] RGB image. Note that height and width\n",
" can be anything since the image will be immediately resized according\n",
" to the needs of the model within this function.\n",
" Returns:\n",
" A Person instance.\n",
" \"\"\"\n",
" image_height, image_width, _ = input_image.shape\n",
"\n",
" input_tensor = self._preprocess(input_image)\n",
"\n",
" self._set_input_tensor(input_tensor)\n",
" self._interpreter.invoke()\n",
"\n",
" # Get all output details\n",
" boxes = self._get_output_tensor(self._OUTPUT_LOCATION_NAME)\n",
" classes = self._get_output_tensor(self._OUTPUT_CATEGORY_NAME)\n",
" scores = self._get_output_tensor(self._OUTPUT_SCORE_NAME)\n",
" count = int(self._get_output_tensor(self._OUTPUT_NUMBER_NAME))\n",
"\n",
" return self._postprocess(boxes, classes, scores, count, image_width,\n",
" image_height)\n",
"\n",
" def _preprocess(self, input_image: np.ndarray) -> np.ndarray:\n",
" \"\"\"Preprocess the input image as required by the TFLite model.\"\"\"\n",
"\n",
" # Resize the input\n",
" input_tensor = cv2.resize(input_image, self._input_size)\n",
"\n",
" # Normalize the input if it's a float model (aka. not quantized)\n",
" if not self._is_quantized_input:\n",
" input_tensor = (np.float32(input_tensor) - self._mean) / self._std\n",
"\n",
" # Add batch dimension\n",
" input_tensor = np.expand_dims(input_tensor, axis=0)\n",
"\n",
" return input_tensor\n",
"\n",
" def _set_input_tensor(self, image):\n",
" \"\"\"Sets the input tensor.\"\"\"\n",
" tensor_index = self._interpreter.get_input_details()[0]['index']\n",
" input_tensor = self._interpreter.tensor(tensor_index)()[0]\n",
" input_tensor[:, :] = image\n",
"\n",
" def _get_output_tensor(self, name):\n",
" \"\"\"Returns the output tensor at the given index.\"\"\"\n",
" output_index = self._output_indices[name]\n",
" tensor = np.squeeze(self._interpreter.get_tensor(output_index))\n",
" return tensor\n",
"\n",
" def _postprocess(self, boxes: np.ndarray, classes: np.ndarray,\n",
" scores: np.ndarray, count: int, image_width: int,\n",
" image_height: int) -> List[Detection]:\n",
" \"\"\"Post-process the output of TFLite model into a list of Detection objects.\n",
" Args:\n",
" boxes: Bounding boxes of detected objects from the TFLite model.\n",
" classes: Class index of the detected objects from the TFLite model.\n",
" scores: Confidence scores of the detected objects from the TFLite model.\n",
" count: Number of detected objects from the TFLite model.\n",
" image_width: Width of the input image.\n",
" image_height: Height of the input image.\n",
" Returns:\n",
" A list of Detection objects detected by the TFLite model.\n",
" \"\"\"\n",
" results = []\n",
"\n",
" # Parse the model output into a list of Detection entities.\n",
" for i in range(count):\n",
" if scores[i] >= self._options.score_threshold:\n",
" y_min, x_min, y_max, x_max = boxes[i]\n",
" bounding_box = Rect(\n",
" top=int(y_min * image_height),\n",
" left=int(x_min * image_width),\n",
" bottom=int(y_max * image_height),\n",
" right=int(x_max * image_width))\n",
" class_id = int(classes[i])\n",
" category = Category(\n",
" score=scores[i],\n",
" label=self._label_list[class_id], # 0 is reserved for background\n",
" index=class_id)\n",
" result = Detection(bounding_box=bounding_box, categories=[category])\n",
" results.append(result)\n",
"\n",
" # Sort detection results by score ascending\n",
" sorted_results = sorted(\n",
" results,\n",
" key=lambda detection: detection.categories[0].score,\n",
" reverse=True)\n",
"\n",
" # Filter out detections in deny list\n",
" filtered_results = sorted_results\n",
" if self._options.label_deny_list is not None:\n",
" filtered_results = list(\n",
" filter(\n",
" lambda detection: detection.categories[0].label not in self.\n",
" _options.label_deny_list, filtered_results))\n",
"\n",
" # Keep only detections in allow list\n",
" if self._options.label_allow_list is not None:\n",
" filtered_results = list(\n",
" filter(\n",
" lambda detection: detection.categories[0].label in self._options.\n",
" label_allow_list, filtered_results))\n",
"\n",
" # Only return maximum of max_results detection.\n",
" if self._options.max_results > 0:\n",
" result_count = min(len(filtered_results), self._options.max_results)\n",
" filtered_results = filtered_results[:result_count]\n",
"\n",
" return filtered_results\n",
"\n",
"\n",
"_MARGIN = 10 # pixels\n",
"_ROW_SIZE = 10 # pixels\n",
"_FONT_SIZE = 1\n",
"_FONT_THICKNESS = 1\n",
"_TEXT_COLOR = (0, 0, 255) # red\n",
"\n",
"\n",
"def visualize(\n",
" image: np.ndarray,\n",
" detections: List[Detection],\n",
") -> np.ndarray:\n",
" \"\"\"Draws bounding boxes on the input image and return it.\n",
" Args:\n",
" image: The input RGB image.\n",
" detections: The list of all \"Detection\" entities to be visualize.\n",
" Returns:\n",
" Image with bounding boxes.\n",
" \"\"\"\n",
" for detection in detections:\n",
" # Draw bounding_box\n",
" start_point = detection.bounding_box.left, detection.bounding_box.top\n",
" end_point = detection.bounding_box.right, detection.bounding_box.bottom\n",
" cv2.rectangle(image, start_point, end_point, _TEXT_COLOR, 3)\n",
"\n",
" # Draw label and score\n",
" category = detection.categories[0]\n",
" class_name = category.label\n",
" probability = round(category.score, 2)\n",
" result_text = class_name + ' (' + str(probability) + ')'\n",
" text_location = (_MARGIN + detection.bounding_box.left,\n",
" _MARGIN + _ROW_SIZE + detection.bounding_box.top)\n",
" cv2.putText(image, result_text, text_location, cv2.FONT_HERSHEY_PLAIN,\n",
" _FONT_SIZE, _TEXT_COLOR, _FONT_THICKNESS)\n",
"\n",
" return image"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {
"cellView": "form",
"colab": {
"base_uri": "https://localhost:8080/",
"height": 401
},
"id": "1t1z2fKlAoB0",
"outputId": "97e3d3d1-3168-4f6c-a754-ca1e6cf4f4a7"
},
"outputs": [
{
"name": "stderr",
"output_type": "stream",
"text": [
"'wget'��(��) ���� �Ǵ� �ܺ� ����, ������ �� �ִ� ���α׷�, �Ǵ�\n",
"��ġ ������ �ƴմϴ�.\n"
]
},
{
"data": {
"image/png": "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/e1v19XVOZ3OP//5zz/5yU9eeOGFr371qx9++OHhw4e3bdumKEpzc7Msy48++mhhYeH777//pS99qaGhYceOHStXrnzmmWdWrVolSVIoFOru7jYMo729/Qc/+MGPfvSj9evXO51OoRn19fU33HDDBx984Ha7V69effz48eeff/7pp59+++23HQ5HR0eHLMuJ4OWI+CpP+7UmwmeC2OAIIKGJ7umHOw0g7nBMAJBlua2t7fnnnzdN84ILLvj2t79dW1vb1dW1du3ajRs3VlVV5eTk9PT05OXlrVixwuVymaY5adIkt9udl5dXUlIyZcoUj8eTk5Pj9/uTk5NFFNBDDz30ve9974orrrjppptuuukmXdfvueee66+/fu7cuQ888IDD4bAsy+PxHDx4MC0tbdmyZZTST33qU08//XRXV1dycvL06dNVVS0sLGxra5s6deq99977qU99avbs2Q8++GBubq5wRg3TBztGYIyJSREIm06HU638uP8QkBigACQc8XTxwjUgzi5hKBSaPn36q6++ap/p6elhjN1yyy1Llizp6urKzMw8cOCA8Ps7nU5JksQqM9M0GWPBYNDhcAAApZQQIsuy3+93u90vvvhiTU3N/fffX1lZ+c1vfvNLX/rSQw899Ic//OHaa6/dtWuXy+WyLCs9Pb2npycYDCYnJ9fX1zudTofDwTkPhUKcc8uyVFVtbW295557/uM//uOZZ5659tpr169fX1xcbBjGODZ/g/0S0ReUgKALKEGJtggRff9BWX8ACAQCHR0dwpQHg8FQKFRcXHzVVVc99dRTmzdv/tWvfrV7925VVdvb20Vcps/n6+npIYSYptnR0QEAhJDOzk7DMIRXx+Vy/fKXv/ziF7/Y1tamaVpGRsaJEyeuuuqqNWvWqKrqdrtlWfZ6vS0tLVOnTj3vvPPuu+++xx9//Kmnnvr85z8vSVJ7e7sIfOzu7jZNs7m5+eqrr37nnXcURRH3JoKvo98vtN+vfuTbhowFcASQKMTZBzytsYhGOBkuvfTSoqIi6OvCi3sfffTRP/7xj++9997s2bMXL17c1NQk4nY451dffXVXVxfnvLi4WPhzAOArX/lKWVmZpmkPPPAA5/ypp5566qmnXnnllTvvvPOee+5xuVzPPPPMyy+/7HQ6X3zxxaSkpJtvvjk1NRUAnnvuuaeeemrnzp3/93//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TwmEEz5csOvhfdnZxJRybW1tRUVFTU2NLMuijJgT3r17d3d3t10DpbS2tvbw4cOigP1Gdu7cKaKAYrQZwqa7RYF//OMfgUBgz549Yplb9OQBAIhHRH84Ynr5o48+qqurE20esi8MQVAAkCFBxPwYhiHSLYiOdnd3t4jlBwBZln0+n90HF8usVFUV05Jer1d0t1VV9fv9wl7v27dv3bp1IuZS1/VQKKSqalNT06pVqyJc50JsTNO0gylVVfV6vaIeMeEZDAabm5s7Ojrs3rQIwtE0jRBiNzIQCJimKRadiRsVRenu7hb9d1FMhHiGv1OR0chujyRJsiz39PSIp1dWVtrLzTZs2LB3717b3ENf7148QtQm2iA+OkJIKBRqbGzs6enBQQAy5OAkMHK2iF7q0aNHt23bxhibNm3a7NmzV61a5fP5FEW54YYb9uzZIxaIORyOG264Ydu2bVVVVT6f75JLLsnLy3v33XeDwWB+fv4ll1yyadOmiooKQsiiRYsOHDhQW1t7/vnne73ebdu2cc4XLlx44sSJ3bt3z507t6GhIScnZ/LkyW+88caSJUs++OCDnp6eFStWFBQUWJa1du3azs5OwzAuueSS9PT0V1991eFwVFVVTZs2raWlJT09/eOPPz5y5AgAJCUlXXfddY2NjRMnTjxw4MCBAwd8Pt/SpUsBoL293TTNN954IxAIeDyeK664oqmpqbi4+LXXXpNlubW1df78+bNmzVq5cqWu64ZhTJ069YILLhAB/mvXru3u7s7MzFy+fLnQtra2NkVR9u7d63K5zjvvPJGojlLa09Ozfv16v9+vadqNN964f//+/fv3E0IuueSSpKSkV199NSkpqa6ubu7cuaP8NSPjERwBIGeLcMusX79+6dKl1157bVdX1/bt24PB4Gc/+1lN07Zv397T05OUlPTpT3+6oaGho6Njx44dkydPXrp0qSRJGzdutCxr+fLlu3fv3rt378GDBz/zmc/Mnj27vb39/PPPnzp1anZ29jvvvDNz5szJkyevWbNm9uzZZWVl5eXlTU1NIttPU1OTYRitra2XX375hAkTxMqD1NTUCy+8kFJ6+PDhPXv2aJp20003ZWRkcM6bm5u9Xm9ra2tOTs5nPvOZurq66urq5ubmYDC4bdu2goKCFStWEEJaW1u7urr27t1LCLn99ttdLldjY2NDQ4Nh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",
"text/plain": [
"<PIL.Image.Image image mode=RGB size=512x338 at 0x1F02F6812B0>"
]
},
"execution_count": 18,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"#@title Run object detection and show the detection results\n",
"\n",
"from PIL import Image\n",
"\n",
"INPUT_IMAGE_URL = \"https://miro.medium.com/max/1400/1*besTuD-m9aktHEJ2VRhCAA.png\" #@param {type:\"string\"}\n",
"DETECTION_THRESHOLD = 0.3 #@param {type:\"number\"}\n",
"TFLITE_MODEL_PATH = \"rico.tflite\" #@param {type:\"string\"}\n",
"\n",
"TEMP_FILE = 'image.png'\n",
"\n",
"!wget -q -O $TEMP_FILE $INPUT_IMAGE_URL\n",
"image = Image.open(TEMP_FILE).convert('RGB')\n",
"image.thumbnail((512, 512), Image.ANTIALIAS)\n",
"image_np = np.asarray(image)\n",
"\n",
"# Load the TFLite model\n",
"options = ObjectDetectorOptions(\n",
" num_threads=4,\n",
" score_threshold=DETECTION_THRESHOLD,\n",
")\n",
"detector = ObjectDetector(model_path=TFLITE_MODEL_PATH, options=options)\n",
"\n",
"# Run object detection estimation using the model.\n",
"detections = detector.detect(image_np)\n",
"\n",
"# Draw keypoints and edges on input image\n",
"image_np = visualize(image_np, detections)\n",
"\n",
"# Show the detection result\n",
"Image.fromarray(image_np)"
]
}
],
"metadata": {
"accelerator": "GPU",
"colab": {
"collapsed_sections": [],
"name": "Model Maker Object Detection for RICO.ipynb",
"provenance": [],
"include_colab_link": true
},
"kernelspec": {
"display_name": "Python 3 (ipykernel)",
"language": "python",
"name": "python3"
},
"language_info": {
"codemirror_mode": {
"name": "ipython",
"version": 3
},
"file_extension": ".py",
"mimetype": "text/x-python",
"name": "python",
"nbconvert_exporter": "python",
"pygments_lexer": "ipython3",
"version": "3.9.7"
}
},
"nbformat": 4,
"nbformat_minor": 0
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment