1 of 39

Toshiki Haraguchi

2023.02.04

2 of 39

はじめに

スペースもあります!

雑談しましょう!

3 of 39

今回やること

  • Anaconda の準備(Python環境の準備)
  • DAMO-YOLOで推論
  • ONNX変換
  • ONNXで推論
  • 前処理と後処理をONNXに入れ込む

リアルタイム推論をするために

Webカメラをご準備ください

PCインカメでもOK

4 of 39

Anacondaの準備

Anacondaとは

Pythonの環境をたくさん作ることができる優れもの

基本的に・・・

普通にインストールする

一つの環境しか作ることができない

色々詰め込むと・・・

環境が爆発して壊れてしまう・・・

複数の環境を並列させることができる

環境作りたい放題!

5 of 39

Anacondaの準備

リンゴさんはこちら

6 of 39

Anacondaの準備

インストーラーに従う

7 of 39

Anacondaの準備

いい感じに設定する

Windowsの場合

PowerShellにて

後、PowerShell再起動し

でOKです

Macの場合

こんな感じに先頭に(base)が出ていればOK

手元のMac環境がちょっとわからず・・・

躓いたら質問してください!

絶対ですよ~~~

8 of 39

今回やること

  • Anaconda の準備(Python環境の準備)
  • DAMO-YOLOで推論
  • ONNX変換
  • ONNXで推論
  • 前処理と後処理をONNXに入れ込む

リアルタイム推論をするために

Webカメラをご準備ください

PCインカメでもOK

9 of 39

DAMO-YOLO

DAMO-YOLOとは

Alibaba DAMO Data Analytics and Intelligence Labが開発したYOLO

DAMOチームが開発したYOLO

YOLOは何ぞや?

You Only Live Once:人生一度きり!

You Only Look Once:一度見るだけ!

YOLOが出るまでの物体検出

物体検出AI

クラス識別

2つもAI使っているので遅い学習めんどくさい

10 of 39

DAMO-YOLO

YOLOは・・・

YOLO

一度AIを通すだけでクラス&物体位置を検出可能

You Only Look Once:一度見るだけ!

ちょこっとだけYOLOの仕組みを説明

  • グリッドの考え方:グリッドレベルで物体位置を割り当て

  • アンカーボックス:よく出るボックスサイズを覚えている

  • 微調整の概念:

11 of 39

DAMO-YOLO

アンカーボックスの考え方

YOLO

ゼロからボックスを予測するのはコストがかかる・・・(どんなボックスを書けばいいかわからん)

そこでアンカーボックス

アンカーボックス:データセットでよく出るボックスサイズを事前に教え込んでおく

例:車検出

事前知識として教える

YOLOは数種類のアンカーボックスを覚えている

がよく出てる

12 of 39

DAMO-YOLO

グリッドの考え方

各グリッドのマス目に対して

YOLO

物体の中心の有無を予測するよ

に物体の中心があると思うよ!

物体があるかどうかは分かったニャ

どうやって物体のサイズわかるニャ?

ここでアンカーボックスが生きてきます!

13 of 39

DAMO-YOLO

に物体があるとして、どのアンカーボックスを使うと検出できそうか

そしてちょっと修正

今回はこれが合いそう

が本当の中心

左上の座標からどれくらいずれているかを予測する必要

は完全に一致しない

立幅と横幅をいい感じに調整する必要

YOLOのイメージ

ざっくり予測してちょこっと修正

14 of 39

とりあえず動かしてみる

YOLOが若干分かったところで

まずはDAMO-YOLOで遊びましょう!

15 of 39

とりあえず動かしてみる

まずは環境構築をしましょう!

git clone https://github.com/tinyvision/damo-yolo

cd DAMO-YOLO/

conda create -n DAMO-YOLO python=3.7 -y

conda activate DAMO-YOLO

conda install pytorch torchvision -c pytorch

pip install -r requirements.txt

公式に従って

を実行し、環境を作りましょう

16 of 39

とりあえず動かしてみる

まずはPyTorchでリアルタイム推論をする

いろんなモデルを改造する際の勘所

  • まずはデモコードを見る
  • モデル呼び出しを探す
  • 前処理後処理を探す
  • えいやーとうと移植しまくる

これを実践してみましょう

17 of 39

とりあえず動かしてみる

つまりtool/demo.pyをのぞき見すれば何かが分かる

main関数にだいたい重要そうなものはいる

origin_img = np.asarray(Image.open(args.path).convert('RGB'))

bboxes, scores, cls_inds = infer_engine.forward(origin_img)

vis_res = infer_engine.visualize(origin_img, bboxes, scores, cls_inds, conf=args.conf,save_name=os.path.basename(args.path), save_result=args.save_result)

if not args.save_result:

  cv2.namedWindow("DAMO-YOLO", cv2.WINDOW_NORMAL)

  cv2.imshow("DAMO-YOLO", vis_res)

312行目から

18 of 39

とりあえず動かしてみる

infer_engine = Infer(config, infer_size=args.infer_size, device=args.device,

        engine_type=args.engine_type, output_dir=args.output_dir, ckpt=args.engine)

よく見ると上にこれがある(307行目)

class Infer():

    def __init__(self, config, infer_size=[640,640], device='cuda', engine_type='torch', output_dir='./', ckpt=None, end2end=False):

�        self.ckpt_path = ckpt

        self.engine_type = engine_type

        self.end2end = end2end # only work with tensorRT engine

        self.output_dir = output_dir

        os.makedirs(self.output_dir, exist_ok=True)

        if torch.cuda.is_available() and device=='cuda':

            self.device = 'cuda'

        else:

            self.device = 'cpu'

�        if "class_names" in config.dataset:

            self.class_names = config.dataset.class_names

        else:

            self.class_names = []

            for i in range(config.model.head.num_classes):

                self.class_names.append(str(i))

            self.class_names = tuple(self.class_names)

�        self.infer_size = infer_size

        config.dataset.size_divisibility = 0

        self.config = config

        self.model = self._build_engine(self.config, engine_type)

config = parse_config(args.config_file)

モデルの呼び出しは「model」の単語を探すと割とあっさり言ったりします

Ctrl+Fで「model」も効果的

24行目

19 of 39

とりあえず動かしてみる

    def _build_engine(self, config, engine_type):

�        print(f'Inference with {engine_type} engine!')

        if engine_type == 'torch':

            model = build_local_model(config, self.device)

            ckpt = torch.load(self.ckpt_path, map_location=self.device)

            model.load_state_dict(ckpt['model'], strict=True)

            for layer in model.modules():

                if isinstance(layer, RepConv):

                    layer.switch_to_deploy()

            model.eval()

        elif engine_type == 'tensorRT':

            model = self.build_tensorRT_engine(self.ckpt_path)

        elif engine_type == 'onnx':

            model, self.input_name, self.infer_size, _, _ = self.build_onnx_engine(self.ckpt_path)

�        return model

あった

64行目

20 of 39

とりあえず動かしてみる

import cv2

from damo.base_models.core.ops import RepConv

from damo.config.base import parse_config

from damo.detectors.detector import build_local_model

from damo.utils import get_model_info, vis

from damo.utils.demo_utils import transform_img

import torch

import numpy as np

from time import time

config = parse_config("./configs/damoyolo_tinynasL20_T.py")

model = build_local_model(config, "cpu")

ckpt = torch.load("./damoyolo_tinynasL20_T.pth", map_location="cpu")

model.load_state_dict(ckpt['model'], strict=True)

for layer in model.modules():

    if isinstance(layer, RepConv):

        layer.switch_to_deploy()

model.eval()

webcam.pyを作成

21 of 39

とりあえず動かしてみる

Model読み込みOK! 推論しているところを探す

bboxes, scores, cls_inds = infer_engine.forward(origin_img)

def forward(self, image):

    image, ratio = self.preprocess(image)

    if self.engine_type == 'torch':

        output = self.model(image)

        bboxes, scores, cls_inds = self.postprocess(output, ratio=ratio)

preprocessして推論してpostprocessしたらOK!!

   def postprocess(self, preds, origin_image=None, ratio=1.0):

�        if self.engine_type == 'torch':

            output = preds

�        bboxes = output[0].bbox * ratio

        scores = output[0].get_field('scores')

        cls_inds = output[0].get_field('labels')

def preprocess(self, origin_img):

�        img = transform_img(origin_img, 0,

                **self.config.test.augment.transform,

                infer_size=self.infer_size)

        img = self._pad_image(img.tensors, self.infer_size)

        # img is a image_list

        ratio = min(origin_img.shape[0] / img.image_sizes[0][0],

            origin_img.shape[1] / img.image_sizes[0][1])

�        img = img.to(self.device)

        return img, ratio

ちょっと画像サイズ変えてるだけ

313行目

212行目

160行目

173行目

22 of 39

とりあえず動かしてみる

   def postprocess(self, preds, origin_image=None, ratio=1.0):

�        if self.engine_type == 'torch':

            output = preds

�        bboxes = output[0].bbox * ratio

        scores = output[0].get_field('scores')

        cls_inds = output[0].get_field('labels')

YOLOの検出において

  • boundingBox
  • score
  • class

犬 score:0.87

Class:名称

score:その物体だと思ったときにどれくらい自信があるか

boundingBox:物体の境界線(左上の座標(xy),右下の座標(xy)の計4つで表しがち)

BBox・Class・Scoreはたびたび出るので

覚えておいてください!

コードで出てきたときになぜ必要か分かり易くなると思います

23 of 39

とりあえず動かしてみる

import cv2

from damo.base_models.core.ops import RepConv

from damo.config.base import parse_config

from damo.detectors.detector import build_local_model

from damo.utils import get_model_info, vis

from damo.utils.demo_utils import transform_img

import torch

import numpy as np

from time import time

config = parse_config("./configs/damoyolo_tinynasL20_T.py")

model = build_local_model(config, "cpu")

ckpt = torch.load("./damoyolo_tinynasL20_T.pth", map_location="cpu")

model.load_state_dict(ckpt['model'], strict=True)

for layer in model.modules():

    if isinstance(layer, RepConv):

        layer.switch_to_deploy()

model.eval()

with torch.no_grad():

        start = time()

        output = model(frame_z)

        end = time()

        bboxes = output[0].bbox

        scores = output[0].get_field('scores')

        cls_inds = output[0].get_field('labels')

webcam.pyに追記

24 of 39

とりあえず動かしてみる

リアルタイムに推論するにはWebカメラとつなぐ必要性

結果を描画するの必要

cap = cv2.VideoCapture(0)

これでWebカメラOpen

ret, frame = cap.read()

これでフレームげっちゅ

vis_img = vis(image, bboxes, scores, cls_inds, conf, self.class_names)

これで描画

入れ込む!(完成系はwebcam.pyを参照)

にお任せ!

OpenCV: 画像処理のほぼすべてをカバーしているライブラリ。

     画像系のことをしたくなったらとりあえずインポートする(ほぼ手癖)

描画はdamo-yoloのものを拝借・・・(242行目)

25 of 39

とりあえず動かしてみる

遅い・・・・・

26 of 39

遅い要因を考える

学習や推論など汎用的にできる

モデルを柔軟に構築できる

推論に特化した構造になっていない

ONNXに変換することで推論の高速化を実現

あいまいな内容にも対応できるようになっているので

27 of 39

ONNX

モデルをいろいろなプラットフォームで実行できるようにするやつ

ONNXに変換するといいこと

モデルがコンパイルされることでCPUで高速に実行することができる

まずはONNXへ変換してみましょう

pip install onnx

pip install onnxruntime

あいまいさの排除

28 of 39

ONNXへ変換

import torch

from damo.config.base import parse_config

from damo.detectors.detector import build_local_model

from damo.base_models.core.ops import RepConv, SiLU

from damo.utils.model_utils import get_model_info, replace_module

import torch.nn as nn

config = parse_config("./configs/damoyolo_tinynasL20_T.py")

model = build_local_model(config, 'cpu')

ckpt = torch.load("damoyolo_tinynasL20_T_418.pth", map_location=torch.device('cpu'))["model"]

model.eval()

model.load_state_dict(ckpt, strict=False)

model = replace_module(model, nn.SiLU, SiLU)

model.head.nms = False

for layer in model.modules():

    if isinstance(layer, RepConv):

        layer.switch_to_deploy()

dummy_input = torch.randn(1, 3, 640, 640)

torch.onnx.export(

    model,

    dummy_input,

    "damo_yolo_T.onnx",

    input_names=["input"],

    output_names=['output_1', 'output_2'],

    opset_version=17,

)

ONNXに対応していないモジュールはいい感じに置き換える必要

ONNX変換の本丸

ダミーを要するのはモデルにデータを流して

モデル構造を確定させる必要があるから(コンパイルみたいな感じ)

input_names:入力の名前。複数ある場合は全部に名前つける

output_names:出力の名前。複数ある場合は全部につける

cvt_onnx.pyを作成

29 of 39

ONNXで推論

ONNXに変換するとNumpyで扱うようになる

model = onnxruntime.InferenceSession("damo_yolo_T.onnx")

モデルの読み込みはこんなに簡単

output = model.run(None,{"input":frame_z})

推論もこれだけ

bboxes = output[1][0]

scores,cls_inds = np.max(output[0][0], axis=1), np.argmax(output[0][0], axis=1)

後処理が消えたので追加

ONNXで推論してみましょう!

30 of 39

ONNXで推論

8400 detection

80 class

各インデックス番号がそのままクラスに紐づいている

各arrayには確率が入っている

0.1

0.8

確率が高い

そのインデックスの

物体である

最大値をとったインデックス・その最大値

scores,cls_inds = np.max(output[0][0], axis=1), np.argmax(output[0][0], axis=1)

1つ目の

検出結果

クラス1(人間)

である確率

クラス2(犬)

である確率

8400個目の

検出結果

クラス80(猫)

である確率

0.1

31 of 39

ONNXで推論

1/5 !?!?!?!?!?!

32 of 39

ONNXで推論

早くなったのはいいけど、

多重に検出するようになってしまった

後処理

Non-Maximum-Suppression

一番確率が高い

一番確率が高いものを

ピックアップ

それと重なり度合いが一定以上

のものは消す

bboxes, scores, cls_inds = nms_fast(bboxes, scores, cls_inds)

33 of 39

ONNXで推論

多重検出は消えた・・・

でも処理遅すぎん????

NMSは使い方注意

8400個全部入れたためめちゃ遅くなった

入れる個数を事前に減らす

34 of 39

ONNXで推論

bboxes = bboxes[scores > conf]

cls_inds = cls_inds[scores > conf]

scores = scores[scores > conf]

conf以上のものだけ取ってくる

絞る前:(8400, 4)

絞った後(9, 4)

この数なら高速にできるのでは!!

confより大きければTrue・そうでなければFalseが入っている

35 of 39

ONNXで推論

もう少し考える・・・

前処理と後処理をいちいち書くのめんどくさい・・・。

モデルに全部入れ込みたい・・・。

できるんです!

にはモデルを鬼カスタムする方法がある

最後に鬼カスタムして前処理・後処理を入れ込みましょう

36 of 39

ONNXで推論

入れ込みたいところを探す

image = image.transpose(2, 0, 1)[np.newaxis].astype(np.float32)

cls_inds, scores = np.argmax(scores, axis=1), np.max(scores, axis=1)

bboxes = bboxes[scores > conf]

cls_inds = cls_inds[scores > conf]

scores = scores[scores > conf]

bboxes, scores, cls_inds = nms_fast(bboxes, scores, cls_inds)

いちいち変換するのめんどっちい

この処理も内部で出来んか???

これらを鬼カスタムモデルにぶち込みます!

37 of 39

鬼カスタムモデルの作り方

class Custom_Model(nn.Module):

    def __init__(self, model):

        super().__init__()

        self.model = model

   

    def preprocess(self,x):

        x = x.permute(2, 0, 1).unsqueeze(0)

        return x

�    def postprocess(self, x, conf):

        bboxes = x[1][0]

        scores, cls_inds = torch.max(x[0][0], dim=1)

        bboxes = bboxes[scores >= conf, :]

        cls_inds = cls_inds[scores >= conf]

        scores = scores[scores >= conf]

        ids = nms(bboxes, scores, 0.8)

        bboxes = bboxes[ids]

        scores = scores[ids]

        cls_inds = cls_inds[ids]

        return bboxes, scores, cls_inds

�    def forward(self, x, conf):

        x = self.preprocess(x)

        x = self.model(x)

        bbox, score, cls_inds = self.postprocess(x, conf)

        return bbox, score, cls_inds

モデル作ります宣言

やりたい作業全部ぶち込み

後処理

前処理

コツ:for文・if文は定数になってしまう

   可変内容は避ける

   テンソルを切るとかはOK

38 of 39

鬼カスタムモデルの作り方

dummy_input = torch.randn(640, 640, 3)

torch.onnx._export(

    all_model,

    (dummy_input, torch.tensor(0.6)),

    "damo_yolo_T.onnx",

    input_names=["input", "conf"],

    output_names=['bbox', 'score', "cls_inds"],

    opset_version=17,

    dynamic_axes={

                          "bbox":{0: "num_det"},

                          "score":{0: "num_det"},

                          "cls_inds":{0: "num_det"},

                         

                      }

)

入出力で可変になる部分があれば入れておくとよい

今回は検出結果が可変になるので検出結果の軸を

可変にしている

前処理に入れ込んだためそのままでOKに

絞る前:(8400, 4)

絞った後(9, 4)

絞った後の検出数は可変

後ろのデータ部分は固定

39 of 39

鬼カスタムモデルの推論

import cv2

from damo.base_models.core.ops import RepConv

from damo.config.base import parse_config

from damo.detectors.detector import build_local_model

from damo.utils import get_model_info, vis

from damo.utils.demo_utils import transform_img

import torch

import numpy as np

from time import time

import onnxruntime

config = parse_config("./configs/damoyolo_tinynasL20_T.py")

cap = cv2.VideoCapture(0)

model = onnxruntime.InferenceSession("damo-yolo_all_onnx.onnx")

conf = 0.6

while True:

    ret, frame = cap.read()

    frame = cv2.resize(frame, (640, 640))

    frame_z = frame.astype(np.float32)

    start = time()

    bboxes, scores, cls_inds = model.run(None,{"input":frame_z, "conf":np.array(conf).astype(np.float32)})

    out_img = vis(frame,

                bboxes,

                scores,

                cls_inds,

                conf=conf,

                class_names=config.dataset.class_names)

    end = time()

    cv2.putText(out_img, f"time:{(end-start)*1000:4.3f}ms",(0,20), cv2.FONT_HERSHEY_SIMPLEX,1.0, (0,255,0), 2 , cv2.LINE_4)

    cv2.imshow("test",out_img)

    cv2.waitKey(1)