ONNX RuntimeとYoloV3で物体検出

ONNX Runtimeを使ってみる。今回はPython APIを使うけど、今後はC/C++も使っていきたい

Requirement

以下のバージョンで動作確認しました

  • Python==3.7
  • numpy==1.16.4
  • Pillow==6.0.0
  • matplotlib==3.1.0
  • onnxruntime==0.4.0

2019/07/08時点、onnxruntimeはPython3.5~Python3.7をサポートしています

Installation

Anaconda for Windowsの仮想環境でテストしました

conda create -n onnxruntime pip python
activate onnxruntime

以下のコマンドで各モジュールをインストール

pip install numpy
pip install Pillow
pip install onnxruntime
pip install matplotlib

Download ONNX Model

onnx/modelsからYOLOv3モデルをダウンロードします

Usage

以降からPythonのコードを書いて『ONNX RuntimeとYoloV3で物体検出』を実践します

画像読込み

YOLOv3モデルに合わせて、画像サイズを(416x416)にリサイズする関数を用意します

In [1]:
import numpy as np
from PIL import Image, ImageDraw

# this function is from yolo3.utils.letterbox_image
def letterbox_image(image, size):
    '''resize image with unchanged aspect ratio using padding'''
    iw, ih = image.size
    w, h = size
    scale = min(w/iw, h/ih)
    nw = int(iw*scale)
    nh = int(ih*scale)

    image = image.resize((nw,nh), Image.BICUBIC)
    new_image = Image.new('RGB', size, (128,128,128))
    new_image.paste(image, ((w-nw)//2, (h-nh)//2))
    return new_image

def preprocess(img):
    model_image_size = (416, 416)
    boxed_image = letterbox_image(img, tuple(reversed(model_image_size)))
    image_data = np.array(boxed_image, dtype='float32')
    image_data /= 255.
    image_data = np.transpose(image_data, [2, 0, 1])
    image_data = np.expand_dims(image_data, 0)
    return image_data

画像の準備

今回使うは、こちらの可愛いフクロウの画像です

owl.jpg

画像リサイズ

画像を読み込んだあと、リサイズ関数を使って画像をリサイズします

In [2]:
# Load image
image = Image.open('img/owl.jpg')
# image = Image.open('img/dog.jpg')

# Resized
image_data = preprocess(image)
image_size = np.array([image.size[1], image.size[0]], dtype=np.int32).reshape(1, 2)

# Check
# print(type(image_data))
# print(image_data)

ONNX Runtimeで物体検出【推論フェーズ】

【フロー】

  1. onnxモデルを読み込みでセッションを作成
  2. 入力名と出力名を取得
  3. 推論を実行

Input to model

モデルに入力するのは、Resized imageとimage sizeです

Resized image (1x3x416x416) Original image size (1x2) which is [image.size[1], image.size[0]]

Output of model

モデルから出力されるのは、boxesとlabelsとscoresです

The model has 3 outputs. boxes: (1x'n_candidates'x4), the coordinates of all anchor boxes, scores: (1x80x'n_candidates'), the scores of all anchor boxes per class, indices: ('nbox'x3), selected indices from the boxes tensor. The selected index format is (batch_index, class_index, box_index). The class list is here

In [3]:
import onnxruntime

# 1.onnxモデルを読み込みセッションを作成
session = onnxruntime.InferenceSession('model/yolov3/yolov3.onnx')

# 2.入力名と出力名を取得
input_name = session.get_inputs()[0].name           # 'image' 取得
input_name_img_shape = session.get_inputs()[1].name # 'image_shape' 取得

output_name_boxes = session.get_outputs()[0].name   # 'boxes' 取得
output_name_scores = session.get_outputs()[1].name  # 'scores' 取得
output_name_indices = session.get_outputs()[2].name # 'indices' 取得

# 3. 推論を実行
outputs_index = session.run([output_name_boxes, output_name_scores, output_name_indices],
                            {input_name: image_data, input_name_img_shape: image_size})

output_boxes = outputs_index[0]
output_scores = outputs_index[1]
output_indices = outputs_index[2]

'''
output_boxes = session.run([output_name_boxes], {input_name: image_data, input_name_img_shape: image_size})[0]
output_scores = session.run([output_name_scores], {input_name: image_data, input_name_img_shape: image_size})[0]
output_indices = session.run([output_name_indices], {input_name: image_data, input_name_img_shape: image_size})[0]
'''
Out[3]:
'\noutput_boxes = session.run([output_name_boxes], {input_name: image_data, input_name_img_shape: image_size})[0]\noutput_scores = session.run([output_name_scores], {input_name: image_data, input_name_img_shape: image_size})[0]\noutput_indices = session.run([output_name_indices], {input_name: image_data, input_name_img_shape: image_size})[0]\n'

推論結果

In [4]:
# print('input_name =', input_name)
# print('input_name =', input_name_img_shape)
print('boxes =', output_boxes)
print('scores =', output_scores)
print('indices =', output_indices)
boxes = [[[-115.28807   -29.733763  -13.145709   71.329445]
  [-204.50719   -36.124626   80.013504   78.54137 ]
  [-231.26309  -212.10149   103.09645   254.13814 ]
  ...
  [ 369.91196   449.67422   380.4828    453.9666  ]
  [ 357.5166    447.38748   390.48685   455.68274 ]
  [ 372.12653   417.64285   382.23645   483.53458 ]]]
scores = [[[2.12223927e-08 2.95622904e-08 0.00000000e+00 ... 1.07360495e-06
   1.61624172e-08 2.49721932e-09]
  [1.60209623e-10 4.61133354e-11 0.00000000e+00 ... 7.29967780e-07
   7.75587239e-09 3.34588357e-10]
  [5.80281601e-08 1.85686133e-10 0.00000000e+00 ... 4.77902540e-06
   5.29711315e-08 1.11467955e-08]
  ...
  [2.36966002e-11 6.49258425e-13 0.00000000e+00 ... 1.51135104e-09
   1.22597044e-10 1.09805498e-11]
  [5.05728792e-11 5.86819482e-12 0.00000000e+00 ... 1.46747947e-09
   1.22064137e-10 1.29283251e-11]
  [3.89910326e-11 1.07585052e-11 0.00000000e+00 ... 7.10325132e-10
   7.71720465e-11 1.44328993e-11]]]
indices = [[  0  14 292]]

出力の定義確認

  • boxes : 全アンカーボックスの座標
  • scores : 全クラスに対する全アンカーボックスのスコア
  • indices : [バッチID クラスID アンカーボックス座標ID]

【補足】scoresについて

今回の場合、アンカーボックス数とクラス数は以下の通りでした

  • ボックス数:10647
  • クラス数:80

そのため、スコア数は以下の式で算出できる

スコア数 = クラス数 × ボックス数

In [5]:
print('num_classes =', len(output_scores[0]))
print('num_boxes =', len(output_scores[0][0]))
num_classes = 80
num_boxes = 10647

推論結果の選定❶

算出されたスコアの中から、高いスコアのみ抽出することで、物体検出を実現します
デフォルトの閾値より高いスコアを抽出したものは、既にindicesに格納してあります

今回の場合、indicesは以下の通りでした

indices = [[ 0 14 292]]

  • バッチID: 0
  • クラスID: 14 (cococ class listより”14”は”bird”)
  • アンカーボックス座標: output_boxes[292] の中身

以下のコードで各項目を抽出できます

In [6]:
'''
print('indices =', output_indices)
print('scores of class14 =', output_scores[0][14])
print('max score of class14 =', output_scores[0][14][292])
print('box of max score =', output_boxes[0][292])
'''

batch_index = output_indices[0][0] # 0
class_index = output_indices[0][1] # 14
box_index = output_indices[0][2]   # 292

print('scores of class14 =', output_scores[batch_index][class_index])
print('max score of class14 =', output_scores[batch_index][class_index][box_index])
print('box of max score =', output_boxes[batch_index][box_index])
scores of class14 = [1.6600055e-11 2.5552893e-12 0.0000000e+00 ... 2.7011581e-07 1.9414585e-09
 2.1305269e-10]
max score of class14 = 0.99722135
box of max score = [ 71.39745 149.73782 304.8433  309.92514]

ただし、上記のやり方だと、indicesの中身を目視確認してから、コードを書くのでスマートではありません

そのため、この部分を自動化するコードが書きます

推論結果の選定❷

In [7]:
out_boxes, out_scores, out_classes = [], [], []
for idx_ in output_indices:
    out_classes.append(idx_[1])
    out_scores.append(output_scores[tuple(idx_)])
    idx_1 = (idx_[0], idx_[2])
    out_boxes.append(output_boxes[idx_1])

print(out_classes)
print(out_scores)
print(out_boxes)
[14]
[0.99722135]
[array([ 71.39745, 149.73782, 304.8433 , 309.92514], dtype=float32)]

トラブルシューティング

ボックス(四角形)の左上頂点をp1, 右下頂点をp2としたときout_boxes座標の定義は以下の通りです

out_boxes[p1_y座標 p1_x座標 p2_y座標 p2_x座標]

xとy座標の定義が逆だと思い、一度失敗しました…

出力画像の生成

最後にボックス・ラベル名・スコアを描画した出力画像を作成します

cocoラベル一覧

算出結果の”14”と”bird”を以下のコードで紐づけします

In [8]:
coco_labels = (
    'person',
    'bicycle',
    'car',
    'motorcycle',
    'airplane',
    'bus',
    'train',
    'truck',
    'boat',
    'traffic light',
    'fire hydrant',
    'stop sign',
    'parking meter',
    'bench',
    'bird',
    'cat',
    'dog',
    'horse',
    'sheep',
    'cow',
    'elephant',
    'bear',
    'zebra',
    'giraffe',
    'backpack',
    'umbrella',
    'handbag',
    'tie',
    'suitcase',
    'frisbee',
    'skis',
    'snowboard',
    'sports ball',
    'kite',
    'baseball bat',
    'baseball glove',
    'skateboard',
    'surfboard',
    'tennis racket',
    'bottle',
    'wine glass',
    'cup',
    'fork',
    'knife',
    'spoon',
    'bowl',
    'banana',
    'apple',
    'sandwich',
    'orange',
    'broccoli',
    'carrot',
    'hot dog',
    'pizza',
    'donut',
    'cake',
    'chair',
    'couch',
    'potted plant',
    'bed',
    'dining table',
    'toilet',
    'tv',
    'laptop',
    'mouse',
    'remote',
    'keyboard',
    'cell phone',
    'microwave',
    'oven',
    'toaster',
    'sink',
    'refrigerator',
    'book',
    'clock',
    'vase',
    'scissors',
    'teddy bear',
    'hair drier',
    'toothbrush')

ボックス・ラベル・スコア描画

In [9]:
import matplotlib.pyplot as plt
%matplotlib inline

# FigureとAxesを作成
fig = plt.figure()
ax = fig.add_subplot(1, 1, 1)

caption = []
draw_box_p = []

for i in range(0, len(out_classes)):
    box_xy = out_boxes[i]
    p1_y = box_xy[0]
    p1_x = box_xy[1]
    p2_y = box_xy[2]
    p2_x = box_xy[3]
    draw_box_p.append([p1_x, p1_y, p2_x, p2_y])
    draw = ImageDraw.Draw(image)
    draw.rectangle(draw_box_p[i], outline=(255, 0, 0), width=5)

    # クラス名とスコア描画
    caption.append(coco_labels[out_classes[i]])
    caption.append('{:.2f}'.format(out_scores[i]))

    ax.text(p1_x, p1_y,
            ': '.join(caption),
            style='italic',
            bbox={'facecolor': 'white', 'alpha': 0.7, 'pad': 10})
    
    caption.clear()

# 画像を表示
img = np.asarray(image)
ax.imshow(img)
Out[9]:
<matplotlib.image.AxesImage at 0x23b03570eb8>

おわりに

もうちょい違うコードの書き方の方がスマートかも…という部分もありますが、まぁ好みもあるので、今回はこんな感じで!
参考になると嬉しいです。

『はやぶさの技術ノート』著者:はやぶさ より