{ "cells": [ { "cell_type": "markdown", "metadata": {}, "source": [ "# SageMaker JumpStart を用いた物体検出 Fine-Tune\n", "Builders Online Series \"Amazon SageMaker JumpStart を用いて機械学習 PoC を IT エンジニアの手で実行する ~ たけのこの里好きに向けたきのこの山検出問題を添えて ~\" \n", "で登壇した内容を再現するコードです。 \n", "前提として、SageMaker Studio で、このノートブックを開き、Image は `Data Science` 、インスタンスは `ml.t3.medium` を選択します。 \n", "\n", "## ディレクトリ構成\n", "* manifest\n", " * output.manifest \n", " `train_raw_images/*.jpg`を SageMaker GroundTruth でラベリングした結果。自身で SageMaker GroundTruth でラベリングする際や、別途用意した画像を使う場合はこのファイルは不要。 \n", " `train_raw_images/*.jpg` をそのまま使い、ラベリングをしたくない場合は使用する\n", "* test_crop_images\n", " `test_raw_images/takenoko.jpg`から 512px x 512px の画像をスライドさせながら切り出した画像を格納するフォルダ。 \n", " `8-2. ベルトコンベアを模した推論`で使用する \n", " 初期状態では空ディレクトリ\n", "* test_detect_images\n", " `test_crop_images/*.png`にある画像を推論した結果を保存するディレクトリ\n", " `8-2. ベルトコンベアを模した推論`で使用する \n", " 初期状態では空ディレクトリ\n", "* test_raw_images\n", " テスト用の画像を配置 \n", " `latitice.png` は格子状にお菓子を配置した画像で、`8-1. 格子状に配置した画像を推論する`で使用する \n", " `takenoko.jpg` はお菓子を直線に配置した画像で、`8-2. ベルトコンベアを模した推論`で使用する \n", "* train_random_crop_images\n", " 学習用にcropした画像を配置する \n", " `5. ラベルと学習データの整形 ` で使用する\n", " 初期状態では空ディレクトリ\n", "* train_raw_images\n", " 学習用の生画像 \n", "\n", "## 手順\n", "* [1. 使用するモジュールのインストールと読み込み](#1.-使用するモジュールのインストールと読み込み)\n", "* [2. 画像収集](#2.-画像収集)\n", "* [3. ラベリングジョブを作成](#3.-ラベリングジョブを作成)\n", "* [4. ラベリング](#4.-ラベリング)\n", "* [5. ラベリング結果をダウンロード](#5.-ラベリング結果をダウンロード)\n", "* [6. ラベルと学習データの整形](#6.-ラベルと学習データの整形)\n", "* [7. 学習する](#7.-学習する)\n", "* [8. デプロイする](#8.-デプロイする)\n", "* [9. 推論する](#9.-推論する)\n", " * [9-1. 格子状に配置した画像を推論する](#9-1.-格子状に配置した画像を推論する)\n", " * [9-2. ベルトコンベアを模した推論](#9-2.-ベルトコンベアを模した推論)\n", "* [10. リソース削除](#10.-リソース削除)\n", "\n", "## 1. 使用するモジュールのインストールと読み込み\n", "* この Notebook で利用するモジュールのインストールと読み込み\n", " * SageMaker Studio で Data Science カーネルを前提としている\n", " * OpenCV が入っていないためインストールする\n", "* 再現できるように seed を固定しておく\n", "* 使用するバケットを設定する(SageMaker の Default bucket を利用)" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "scrolled": true }, "outputs": [], "source": [ "!apt-get update && apt-get upgrade -y\n", "!apt-get install libgl1-mesa-dev -y\n", "!pip install opencv-python\n", "\n", "import sagemaker, json, numpy as np, os, boto3, uuid\n", "from PIL import Image, ImageDraw, ImageOps, ImageColor, ImageFont\n", "import matplotlib.patches as patches\n", "from matplotlib import pyplot as plt\n", "from glob import glob\n", "import cv2\n", "np.random.seed(seed=1234)\n", "bucket = sagemaker.session.Session().default_bucket()" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## 2. 画像収集\n", "* 今回は`./train_raw_images`に格納済\n", "* このリポジトリに含まれる画像を使う場合は下記セルでファイルの有無を確認する\n", "* 自身のデータを利用するには`./train_raw_images/*.jpg`にある画像を差し替えておく" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "# ファイル有無確認\n", "TRAIN_RAWIMAGE_DIR = './train_raw_images/'\n", "print(*sorted(glob(TRAIN_RAWIMAGE_DIR+'*.jpg')))" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## 3. SageMaker GroundTruth でラベリング\n", "\n", "以下は自身でラベリングする場合の記述で、ラベリングを行わない場合(ラベリング済のデータを使う場合)は、[6. ラベルと学習データの整形](#6.-ラベルと学習データの整形) をクリックして以下は省略する\n", "\n", "### 3-1. ラベリング対象を S3 にアップロード\n", "* SageMaker GroundTruth でラベリングをする際は、予め S3 にアップロードする必要がある\n", "* 以下はアップロードする(とアップロード先に事前にファイルがあった場合の削除する)コマンド\n", "* `print()`の出力結果は SageMaker GroundTruth でラベリングジョブを作成するのに使用する" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "scrolled": true }, "outputs": [], "source": [ "BASE_PREFIX = 'takenoko_kinoko_gt'\n", "GT_JOB_NAME = f'{BASE_PREFIX}-{uuid.uuid4()}'.replace('_','-')\n", "!aws s3 rm s3://{bucket}/{BASE_PREFIX} --recursive\n", "rawimage_s3_uri = sagemaker.session.Session().upload_data(TRAIN_RAWIMAGE_DIR,key_prefix=BASE_PREFIX)" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "print(f'GroundTruth job name : {GT_JOB_NAME}')\n", "print(f'GroundTruth Target : {rawimage_s3_uri}/')\n", "print(f'GroundTruth Role : {sagemaker.get_execution_role()}')" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## 3. ラベリングジョブを作成\n", "\n", "以下は自身でラベリングする場合の記述で、ラベリングを行わない場合(ラベリング済のデータを使う場合)は、[6. ラベルと学習データの整形](#6.-ラベルと学習データの整形) をクリックして以下は省略する\n", "\n", "### 3-1. ラベリング用のプライベートチームを作成\n", "1. [ラベリングワークフォースの画面](https://ap-northeast-1.console.aws.amazon.com/sagemaker/groundtruth?region=ap-northeast-1#/labeling-workforces)にアクセスし、`プライベート` タブをクリックし、`プライベートチームを作成` をクリック\n", " 上記リンクは`ap-northeast-1`に飛ぶリンクなので、違うリージョンを使っている場合は適宜修正\n", " ![](https://raw.githubusercontent.com/kazuhitogo/builders-online-202201-demo/main/images_for_ipynb/create_private_team.png)\n", "2. `AWS Cognito` でプライベートチームを作成するにチェックを入れ、チーム名に任意の名前(ex:kinoko-detect-team)を入力し、`E メールによる新しいワーカーの招待` にチェックを入れ、 `E メールアドレス`にラベリングさせたい人のメールアドレスをカンマ区切りで入力、`組織名`に任意の名前(ex:茸探隊)を入力、`連絡先 E メール` にチームを管理する人のメールアドレスを入力し、`プライベートチームを作成`をクリック \n", " ![](https://raw.githubusercontent.com/kazuhitogo/builders-online-202201-demo/main/images_for_ipynb/create_private_team_param1.png) \n", " ![](https://raw.githubusercontent.com/kazuhitogo/builders-online-202201-demo/main/images_for_ipynb/create_private_team_param2.png) \n", "3. プライベートワークフォースの概要の画面に `ラベリングポータルのサインイン URL` が表示されるので、その URL をクリックする。 \n", " ![](https://raw.githubusercontent.com/kazuhitogo/builders-online-202201-demo/main/images_for_ipynb/private_team_workforce.png) \n", "4. しばらくすると別途ワーカー宛にメールが送信されるので、そこに書いてあるパスワードをコピーする \n", " ![](https://raw.githubusercontent.com/kazuhitogo/builders-online-202201-demo/main/images_for_ipynb/cognito_mail.png) \n", "5. 3 で出現するログイン画面にワーカーのメールアドレスとメールで送られてきたパスワードを入力し、 `Sign in` をクリック \n", " ![](https://raw.githubusercontent.com/kazuhitogo/builders-online-202201-demo/main/images_for_ipynb/cognito_signin.png) \n", "6. パスワード変更を求められるので新しいパスワードを入力し、`Send` をクリック \n", " ![](https://raw.githubusercontent.com/kazuhitogo/builders-online-202201-demo/main/images_for_ipynb/cognito_change_pw.png) \n", "7. ラベリングジョブのポータルに遷移する。まだラベリングジョブが無いので以降でラベリングジョブを作成する \n", " ![](https://raw.githubusercontent.com/kazuhitogo/builders-online-202201-demo/main/images_for_ipynb/labeling_job_portal.png) " ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### 3-2. ラベリングジョブの作成\n", "\n", "1. [SageMaker GroundTruth のラベリングジョブ作成画面](https://ap-northeast-1.console.aws.amazon.com/sagemaker/groundtruth?region=ap-northeast-1#/labeling-jobs)にアクセスし、`ラベリングジョブの作成` をクリック \n", " ![](https://raw.githubusercontent.com/kazuhitogo/builders-online-202201-demo/main/images_for_ipynb/labeling_job_list.png) \n", "2. ラベリングジョブの詳細を設定する\n", " 1. `ジョブ名`に上のセルの `GroundTruth job name :` 以降の文字列を記入 \n", " 2. `入力データセットの S3 の場所`に上のセルの`GroundTruth Target :`以降の文字列を記入 \n", " 3. `出力データセットの S3 の場所` はデフォルトのまま`入力データセットと同じ場所` を選択 \n", " 4. `データタイプ`は`画像`を選択 \n", " 5. `IAM ロール`は今動かしている SageMaker Studio と同じロールを選択(上のセルの `GroundTruthe Role : `以降に表示されているロールを選択) \n", " 6. `完全なデータセットアップ`をクリック \n", " ![](https://raw.githubusercontent.com/kazuhitogo/builders-online-202201-demo/main/images_for_ipynb/labeling_job_param.png) \n", "3. `タスクカテゴリ`で`画像`を選択肢、`タスクの選択`で、`境界ボックス`を選択 \n", " ![](https://raw.githubusercontent.com/kazuhitogo/builders-online-202201-demo/main/images_for_ipynb/labeling_job_param2.png) \n", "4. `次へ`をクリック \n", " ![](https://raw.githubusercontent.com/kazuhitogo/builders-online-202201-demo/main/images_for_ipynb/labeling_job_param3.png) \n", " 9. `ワーカータイプ`で`プライベートチーム`を選択 \n", " 10. `プライベートチーム`のプルダウンで作成済のワーカーを選択(要事前に作成) \n", " ![](https://raw.githubusercontent.com/kazuhitogo/builders-online-202201-demo/main/images_for_ipynb/select_worker_and_tool1.png) \n", "12. 他はデフォルトのまま、`境界ボックスラベリングツール`の部分で、`take`と`kino`というラベルを作成 \n", " 初期の空白ラベルは1つなので、`take`を入力後、`新しいラベルを追加`をクリックして、`kino` を入力 \n", "13. `作成`をクリック \n", " ![](https://raw.githubusercontent.com/kazuhitogo/builders-online-202201-demo/main/images_for_ipynb/select_worker_and_tool2.png) \n", "\n" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## 4. ラベリング\n", "1. しばらくすると `3-1.7` でログインした画面にジョブが表示される(更新ボタンを押さないと反映されないので更新ボタンを定期的に押下してください)ので、作成したジョブのラジオボタンを活性化させて、`Start working`をクリック \n", " ![](https://raw.githubusercontent.com/kazuhitogo/builders-online-202201-demo/main/images_for_ipynb/start_working.png) \n", "2. プライベートチームにログインして、タケノコ状のお菓子と、キノコ状のお菓子をそれぞれ矩形で括る \n", " ![](https://raw.githubusercontent.com/kazuhitogo/builders-online-202201-demo/main/images_for_ipynb/labeling.png) \n", "3. labeling ジョブが完了するとポータル画面に戻る \n", " ![](https://raw.githubusercontent.com/kazuhitogo/builders-online-202201-demo/main/images_for_ipynb/labeling_job_portal.png)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## 5. ラベリング結果をダウンロード\n", "* ラベリング結果である`output.manifest`をダウンロードする。\n", " * このリポジトリにはあらかじめ`output.manifest`を`./manifest/output.manifest`に用意しているので、ラベリングを割愛したい場合はそちらを使う(下のセルはコメントアウトのまま)\n", " * ラベリングをした場合は下のセルのコメントアウトを外す" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "# sagemaker.session.Session().download_data('./manifest/',key_prefix=f'{BASE_PREFIX}/{GT_JOB_NAME}/manifests/output/output.manifest',bucket=bucket)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## 6. ラベルと学習データの整形\n", "* SSD MobileNet 1.0 で転移学習するために`./manifest/output.manifest`を加工して、`manifest.json`を作成する\n", "* データ形式は、以下を遵守する必要がある(下記文章はモデルを選択した際の最下部に記載あり)\n", "```\n", "The annotations.json file should should have information for bounding_boxes and their class labels. It should have a dictionary with keys \"images\" and \"annotations\". Value for the \"images\" key should be a list of entries, one for each image of the form {\"file_name\": image_name, \"height\": height, \"width\": width, \"id\": image_id}. Value of the 'annotations' key should be a list of entries, one for each bounding box of the form {\"image_id\": image_id, \"bbox\": [xmin, ymin, xmax, ymax], \"category_id\": bbox_label}.\n", "```\n", "* 学習データと`manifest.json`はS3に最終的に配置する必要があるが、S3でのデータの配置は以下を遵守する必要がある\n", "```\n", "input_directory\n", " |--images\n", " |--abc.png\n", " |--def.png\n", " |--annotations.json \n", "```\n", "* データが少ないのでランダムクロップを行う\n", " * 学習用の生データは 1147 x 1108 のため、お菓子が少なくとも 2 個以上写っている場所をランダムに 512 x 512 で切り取る\n", " * 切り取る枚数は各画像から 20 枚\n", " * ラベリングでくくった矩形の面積の 1/4 以上写っていればお菓子が写った、とカウントする\n", "* 512 x 512 に切り取った画像は、ラベリングの情報と当然ずれるので、切り取った座標位置を用いて補正する\n", "\n", "\n", "これらの処理を以下で行い、512 x 512 の画像を`./train_random_crop_images`に、ラベリングデータを`./manifest.json`に出力し、それらを`s3://sagemaker-{region}-{account}/train_randcrop`以下に保存する" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "# クロップした画像にきのこの山やたけのこの里が映っている場合、\n", "# クロップした後のきのこの山やたけのこの里が1/4以下かどうかを判定するヘルパー関数\n", "\n", "def fix_bbox(l,t,r,b,w,h):\n", " # 判定結果、NG なら False にする\n", " judge = True\n", " # ラベリング結果のクロップ補正後の値が負の値ならば 0 に、イメージサイズより大きければイメージサイズに補正する\n", " fix_left = 0 if l < 0 else l\n", " fix_top = 0 if t < 0 else t\n", " fix_right = w if r > w else r\n", " fix_bottom = h if b > h else b\n", " # 領域外ならラベリング無しとする\n", " if l > w or t > h or r < 0 or b <0:\n", " judge=False\n", " # 基の面積の1/4以下ならアノテーション無しとする\n", " elif (r-l)*(b-t)/4 > (fix_right-fix_left)*(fix_bottom-fix_top):\n", " judge=False\n", " \n", " return judge,(fix_left,fix_top,fix_right,fix_bottom)\n", " " ] }, { "cell_type": "code", "execution_count": null, "metadata": { "scrolled": true }, "outputs": [], "source": [ "# ラベリング結果をテキストとして読み込む\n", "with open('manifest/output.manifest','r') as f:\n", " manifest_line_list = f.readlines()\n", "\n", "# クロップした結果のきのこの山やたけのこの里の位置情報を格納する辞書 \n", "annotation_dict = {\n", " 'images':[],\n", " 'annotations':[]\n", "}\n", "\n", "# クロップサイズの定数\n", "IMAGE_SIZE_TUPLE=(512,512)\n", "\n", "# クロップした画像のファイル名に使う一意なシーケンス番号\n", "IMAGE_ID = 0\n", "\n", "# クロップした画像の保存先\n", "OUTPUT_DIR = './train_random_crop_images/'\n", "\n", "# (re-run用の削除コマンド)\n", "!rm -rf {OUTPUT_DIR}*.png\n", "\n", "# ラベリング結果の行数分ループする\n", "# ラベリング結果は 1 行につき 1 画像格納される\n", "for manifest_line in manifest_line_list:\n", " # 画像のラベリング結果の読み込み\n", " manifest_dict = json.loads(manifest_line)\n", " # 画像のファイル名取得(ラベリング結果に格納されている)\n", " filename = manifest_dict['source-ref'].split('/')[-1]\n", " # 元画像のサイズを取得(ラベリング結果に格納されている)\n", " image_size_tuple=(manifest_dict['kinoko-takenoko-aug']['image_size'][0]['width'],manifest_dict['kinoko-takenoko-aug']['image_size'][0]['height'])\n", " # PIL で画像を開く\n", " raw_img = Image.open(os.path.join(TRAIN_RAWIMAGE_DIR,filename))\n", " # 20 回クロップする\n", " for i in range(20):\n", " # ループするかどうかのフラグ(画像にきのこの山やたけのこの里が 2 枚未満だったらクロップをやりなおし)\n", " loop = True\n", " while loop:\n", " # クロップを行う左上の座標を設定\n", " rand_x = np.random.randint(0,image_size_tuple[0]-IMAGE_SIZE_TUPLE[0])\n", " rand_y = np.random.randint(0,image_size_tuple[1]-IMAGE_SIZE_TUPLE[1])\n", " # クロップする\n", " crop_img = raw_img.crop((\n", " rand_x,\n", " rand_y,\n", " rand_x + IMAGE_SIZE_TUPLE[0],\n", " rand_y + IMAGE_SIZE_TUPLE[1]\n", " ))\n", " # クロップ後のきのこの山やたけのこの里の位置を格納するリスト\n", " annotation_list = []\n", " # 元画像のラベリング結果をループ\n", " for annotation in manifest_dict['kinoko-takenoko-aug']['annotations']:\n", " # クロップした後のきのこの山やたけのこの里の座標に補正\n", " left = annotation['left'] - rand_x\n", " top = annotation['top'] - rand_y\n", " right = annotation['left'] + annotation['width'] - rand_x\n", " bottom = annotation['top'] + annotation['height'] - rand_y\n", " # きのこの山やたけのこの里があるかどうかを判定\n", " judge,(left,top,right,bottom) = fix_bbox(left,top,right,bottom,IMAGE_SIZE_TUPLE[0],IMAGE_SIZE_TUPLE[1])\n", " if judge:\n", " # きのこの山やたけのこの里があったら位置とラベルを追加\n", " annotation_list.append(\n", " {\n", " 'bbox':[left,top,right,bottom],\n", " 'category_id':annotation['class_id']\n", " }\n", " )\n", " # きのこの山やたけのこの里と数が2未満だったらクロップやり直し\n", " if len(annotation_list) > 1:\n", " loop = False\n", " \n", " # クロップしたら画像を保存する\n", " save_file_name = f'{str(IMAGE_ID).zfill(5)}_{str(i).zfill(5)}_{filename}'.replace('jpg','png')\n", " crop_img.save(os.path.join(OUTPUT_DIR,save_file_name))\n", " \n", " # 補正済ラベリング結果を出力用辞書に格納\n", " annotation_dict['images'].append(\n", " {\n", " 'file_name' : save_file_name,\n", " 'height' : IMAGE_SIZE_TUPLE[1],\n", " 'width' : IMAGE_SIZE_TUPLE[0],\n", " 'id' : IMAGE_ID\n", " }\n", " )\n", " for annotation in annotation_list: \n", " annotation_dict['annotations'].append(\n", " {\n", " 'image_id': IMAGE_ID,\n", " 'bbox':annotation['bbox'],\n", " 'category_id':annotation['category_id']\n", " }\n", " )\n", " IMAGE_ID += 1\n", "\n", "# ランダムクロップ補正後のラベリング結果を出力\n", "with open('annotations.json','wt') as f:\n", " f.write(json.dumps(annotation_dict)) \n", "\n", "# 出力したディレクトリを prefix として使う\n", "prefix = OUTPUT_DIR[2:-1]\n", "# re-run 用の削除コマンド\n", "!aws s3 rm s3://{bucket}/{prefix} --recursive\n", "# ランダムクロップした画像をアップロード \n", "image_s3_uri = sagemaker.session.Session().upload_data(OUTPUT_DIR,key_prefix=f'{prefix}/images')\n", "# ラベリング結果をアップロード\n", "annotatione_s3_uri = sagemaker.session.Session().upload_data('./annotations.json',key_prefix=prefix)\n", "# Fine-Tune で使う URI を出力\n", "paste_str = image_s3_uri.replace('/images','')\n", "print(f\"paste string to S3 bucket address:{paste_str}\")" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## 7. 学習する\n", "SageMaker JumpStart で転移学習を行う\n", "\n", "1. SageMaker Studio の左上にある `+` アイコンなどから Launcher タブを出し、`Get started` の`Explore one-click solutions, models, and tutorials SageMaker JumpStart`の中にある`Go to SageMaker JumpStart` をクリックする \n", " ![](https://raw.githubusercontent.com/kazuhitogo/builders-online-202201-demo/main/images_for_ipynb/launch_get_started.png) \n", " ![](https://raw.githubusercontent.com/kazuhitogo/builders-online-202201-demo/main/images_for_ipynb/go_to_sagemaker_jumpstart.png) \n", "2. `Search` と書かれた検索窓に `SSD MobileNet` と打ち込むと青い丸に`m`と書かれたアイコンで SSD MobileNet 1.0 と表示されるので、それをクリックする \n", " ![](https://raw.githubusercontent.com/kazuhitogo/builders-online-202201-demo/main/images_for_ipynb/ssd_mobilenet.png) \n", "3. やや下にあるFine-tune Model のData Source のラジオボタンを `Enter S3 bucket location` を選択し、`S3 bucket address` に上のセルの `paste string to S3 bucket address: `より後ろの値を貼り付ける\n", "4. `Deployment Configuration` をクリックし、`SageMaker Training Instance` で `ML.G4dn.xlarge`を選択する。`Model Name` は任意の名前を入力する(デフォルトのままでも可だが、わからなくなりやすいので、検出したいモノなどを入れるとよい。例:`kinoko-detection-model` など)\n", "5. `Hyper-parameters`をクリックし、下記を入力する。(デフォルトのままだと学習が短く急なため変更)\n", " * learning rate : 0.0001\n", " * batch-size : 4\n", " * epochs : 40 \n", "6. `Train` をクリックして Fine-Tune をスタートさせる。\n", "\n", "![](https://raw.githubusercontent.com/kazuhitogo/builders-online-202201-demo/main/images_for_ipynb/train_specification.png) " ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## 8. デプロイする\n", "完了するとデプロイできるようになるのでデプロイする。\n", "1. `Deployment Configuration`をクリックする\n", "2. `SageMaker Hosting Instance`のプルダウンで`ML.M5.Large`を選択する\n", "3. Endpoint Name に任意の名前を入力する(ex: `kinoko-detection-endpoint`)\n", "4. `Deploy` をクリックする \n", " ![](https://raw.githubusercontent.com/kazuhitogo/builders-online-202201-demo/main/images_for_ipynb/deploy_specification.png) \n", "5. In Serviceに変わることを確認する(デプロイ完了確認) \n", " ![](https://raw.githubusercontent.com/kazuhitogo/builders-online-202201-demo/main/images_for_ipynb/deploy_completed.png) " ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## 9. 推論する\n", "### 9-1. 格子状に配置した画像を推論する\n", "* 出来上がったモデルで推論を行う\n", "* request の Body に `open` でメモリに展開したバイナリ情報を入れて推論する\n", "* `ENDPOINT_NAME` 変数に上の画像の Endpoint Name の文字列を入力して、エンドポイントを指定する\n", "* `test_raw_images/lattice.png`を利用して、推論する。推論結果も併せて画像に矩形を描画して可視化する" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "# 推論エンドポイントにアクセスするための sagemaker-runtime クライアントの生成\n", "smr_client = boto3.client('sagemaker-runtime')\n", "# エンドポイントの名前\n", "ENDPOINT_NAME='jumpstart-ftc-kinoko-detection-endpoint'\n", "# 推論する画像の場所\n", "TEST_IMAGE_FILE = 'test_raw_images/lattice.jpg'" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "# 推論対象の画像を開いて変数に格納\n", "with open(TEST_IMAGE_FILE, 'rb') as f:\n", " img_bin = f.read()\n", "\n", "# 推論を実行\n", "response = smr_client.invoke_endpoint(EndpointName=ENDPOINT_NAME, ContentType='application/x-image', Body=img_bin)\n", "\n", "# 推論結果を読み込む\n", "model_predictions = json.loads(response['Body'].read())\n", "\n", "# 結果を可視化\n", "# テスト画像を PIL を通して numpy array として開く\n", "image_np = np.array(Image.open(TEST_IMAGE_FILE))\n", "# matplotlibで描画する\n", "fig = plt.figure(figsize=(20,20))\n", "ax = plt.axes()\n", "ax.imshow(image_np)\n", "# 推論結果を変数に展開\n", "bboxes, classes, confidences = model_predictions['normalized_boxes'], model_predictions['classes'], model_predictions['scores']\n", "# 物体検出結果を検出した分だけループする\n", "for idx in range(len(bboxes)):\n", " # 信頼度スコアが 0.5 以上のみ可視化する\n", " if confidences[idx]>0.8:\n", " # 検出した座標(左上を(0,0),右下を(1,1)とした相対座標)を取得\n", " left, bot, right, top = bboxes[idx]\n", " # 相対座標を絶対座標に変換する\n", " x, w = [val * image_np.shape[1] for val in [left, right - left]]\n", " y, h = [val * image_np.shape[0] for val in [bot, top - bot]]\n", " # 検出した物体の ID を take/kino に読み替える\n", " class_name = 'take' if int(classes[idx])==0 else 'kino'\n", " # take/kinoに対して矩形で描画するための色を設定する\n", " color = 'blue' if class_name == 'take' else 'red'\n", " # matplotlib に検出した物体に矩形を描画する\n", " rect = patches.Rectangle((x, y), w, h, linewidth=3, edgecolor=color, facecolor='none')\n", " ax.add_patch(rect)\n", " # 左上に検出結果と信頼度スコアを描画する\n", " ax.text(x, y, \"{} {:.0f}%\".format(class_name, confidences[idx]*100), bbox=dict(facecolor='white', alpha=0.5))\n", "fig" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### 9-2. ベルトコンベアを模した推論\n", "* `./test_raw_images/takenoko.jpg` にタケノコが横一列に並んでいる(1つだけキノコが混在)\n", "* 512x512の画像をスライドしながら切り出すことでベルトコンベアでお菓子が流れているような動画として扱う\n", "* 各画像に対して推論をかけ、最後に1つの動画として出力する" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "%%time\n", "# 画像を切り出すためのコード\n", "# 開始地点設定\n", "x ,y = 0,512\n", "# 切り出すサイズ設定\n", "CROP_SIZE=(512,512)\n", "# 切り出す対象の画像を PIL で開く\n", "img = Image.open('./test_raw_images/takenoko.jpg')\n", "# 切り出した画像を保存するディレクトリ\n", "CROP_DIR = './test_crop_images/'\n", "# re-run 用の削除コマンド\n", "!rm -rf {CROP_DIR}/*.png\n", "# 1pxずらしてループ\n", "for i in range(img.size[0]-CROP_SIZE[0]):\n", " # 画像の切り出し\n", " crop_img = img.crop((i,y,i+CROP_SIZE[0],y+CROP_SIZE[1]))\n", " # 切り出した画像を保存\n", " file_name = f'{CROP_DIR}{str(i).zfill(5)}.png'\n", " crop_img.save(file_name)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "全データ推論して、矩形を描いた画像を生成する" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "%%time\n", "# 検出結果を保存するディレクトリを設定\n", "DETECT_DIR='./test_detect_images/'\n", "# re-run 用の削除コマンド\n", "!rm -rf {DETECT_DIR}/*.png\n", "\n", "# 切り出した画像分だけループ\n", "for img_file_path in sorted(glob(f'{CROP_DIR}*.png')):\n", " # 切り出した画像を開く\n", " with open(img_file_path,'rb') as f:\n", " img_bin = f.read()\n", " # 推論エンドポイントに画像を投げる\n", " response = smr_client.invoke_endpoint(EndpointName=ENDPOINT_NAME, ContentType='application/x-image', Body=img_bin)\n", " # 推論結果を読み込む\n", " pred=json.loads(response['Body'].read())\n", " # 推論結果を展開\n", " bboxes, classes, confidences = pred['normalized_boxes'], pred['classes'], pred['scores']\n", " # 切り出した画像を PIL で開く\n", " img = Image.open(img_file_path)\n", " # 矩形やテキストを描くために draw インスタンスを生成\n", " draw = ImageDraw.Draw(img)\n", " # 検出したkino/take分ループ\n", " for i in range(len(bboxes)):\n", " # 信頼度スコアが0.8以上のみ描画する\n", " if confidences[i]>0.8:\n", " # 矩形の相対座標を取得\n", " left, top, right, bottom = bboxes[i]\n", " # 矩形の相対座標を絶対座標に変換\n", " left = img.size[0] * left\n", " top = img.size[1] * top\n", " right = img.size[0] * right\n", " bottom = img.size[1] * bottom\n", " # 検出した物体の ID を take/kino に読み替える\n", " text = 'take' if int(classes[i])==0 else 'kino'\n", " # take/kinoに対して矩形で描画するための色を設定する\n", " color = 'blue' if text == 'take' else 'red'\n", " # 矩形の左上に表示する文字の大きさを設定、きのこの山なら大きくする\n", " TEXTSIZE=14 if classes[i]=='0' else 18\n", " # 矩形の先の太さを設定、きのこの山なら太くする\n", " LINEWIDTH=4 if classes[i]=='0' else 6\n", " # 矩形を描画する\n", " draw.rectangle([(left,top),(right,bottom)], outline=color, width=LINEWIDTH)\n", " # 矩形の左上に描画する信頼度スコアの取得\n", " text += f' {str(round(confidences[i],3))}'\n", " # テキストを描画する場所を取得\n", " txpos = (left, top-TEXTSIZE-LINEWIDTH//2)\n", " # フォントの設定\n", " font = ImageFont.truetype(\"/usr/share/fonts/truetype/noto/NotoMono-Regular.ttf\", size=TEXTSIZE)\n", " # 描画するテキストのサイズを取得\n", " txw, txh = draw.textsize(text, font=font)\n", " # テキストの背景用の矩形を描画\n", " draw.rectangle([txpos, (left+txw, top)], outline=color, fill=color, width=LINEWIDTH)\n", " # テキストを描画\n", " draw.text(txpos, text, fill='white',font=font)\n", " # 画像をファイルに書き出す\n", " img.save(img_file_path.replace(CROP_DIR,DETECT_DIR))" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "画像を動画に書き込む" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "%%time\n", "\n", "fourcc = cv2.VideoWriter_fourcc('m', 'p', '4', 'v')\n", "video = cv2.VideoWriter('./video.mp4',fourcc, 120.0, CROP_SIZE)\n", "for img_file_path in sorted(glob(f'{DETECT_DIR}*.png')):\n", " img = cv2.imread(img_file_path)\n", " video.write(img)\n", "video.release()" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## 10. リソース削除\n", "* 現時点で進行中の主な課金対象はSageMaker Hosting による推論エンドポイントの時間課金と、`SageMaker Studio` のインスタンスの時間課金が対象\n", " * 他にも `Training` での成果物である S3 に出力されたモデルや、SageMaker Studio が使用している EFS のストレージも課金対象であるが、微々たるものなので、ここで詳細は割愛する\n", " * 削除する場合は適宜 Studio のターミナルで s3 rm や rm コマンドを利用して削除する\n", "* 推論エンドポイントを停止するのは API やマネジメントコンソールからもできるが、SageMaker Studio から止めることも可能\n", "* ここでは SageMaker Studio から停止する方法を紹介\n", "1. エンドポイントの停止\n", " 1. `Deploy` した後に表示される画面を表示させる。消えてしまっている場合は画面左のテトラポッド型のアイコンをクリックし、Model Endpoints を選択、`In Service` になっているEndpoint をダブルクリックする \n", " ![](https://raw.githubusercontent.com/kazuhitogo/builders-online-202201-demo/main/images_for_ipynb/select_active_endpoint.png) \n", " 2. `Delete` をクリックする \n", " ![](https://raw.githubusercontent.com/kazuhitogo/builders-online-202201-demo/main/images_for_ipynb/delete_endpoint.png) \n", " 3. 再度 `Delete` をクリックする \n", " ![](https://raw.githubusercontent.com/kazuhitogo/builders-online-202201-demo/main/images_for_ipynb/delete_endpoint_confirm.png) \n", " 4. 削除されたことを確認する \n", " ![](https://raw.githubusercontent.com/kazuhitogo/builders-online-202201-demo/main/images_for_ipynb/deleted_endpoint.png) \n", "2. インスタンスの停止 \n", " 1. 左にある停止アイコンをクリックし、RUNNING INSTANCES の下にある電源ボタンをクリックする \n", " ![](https://raw.githubusercontent.com/kazuhitogo/builders-online-202201-demo/main/images_for_ipynb/stop_instance.png) \n", " 2. インスタンスやノートブックの表示が消えることを確認する" ] } ], "metadata": { "instance_type": "ml.t3.medium", "kernelspec": { "display_name": "Python 3 (Data Science)", "language": "python", "name": "python3__SAGEMAKER_INTERNAL__arn:aws:sagemaker:us-east-2:429704687514:image/datascience-1.0" }, "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.7.10" } }, "nbformat": 4, "nbformat_minor": 4 }