Toshiki Haraguchi
2023.02.04
はじめに
スペースもあります!
雑談しましょう!
今回やること
リアルタイム推論をするために
Webカメラをご準備ください
PCインカメでもOK
Anacondaの準備
Anacondaとは
Pythonの環境をたくさん作ることができる優れもの
基本的に・・・
普通にインストールする
一つの環境しか作ることができない
色々詰め込むと・・・
環境が爆発して壊れてしまう・・・
複数の環境を並列させることができる
環境作りたい放題!
Anacondaの準備
リンゴさんはこちら
Anacondaの準備
インストーラーに従う
Anacondaの準備
いい感じに設定する
Windowsの場合
PowerShellにて
後、PowerShell再起動し
でOKです
Macの場合
こんな感じに先頭に(base)が出ていればOK
手元のMac環境がちょっとわからず・・・
躓いたら質問してください!
絶対ですよ~~~
今回やること
リアルタイム推論をするために
Webカメラをご準備ください
PCインカメでもOK
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使っているので遅い&学習めんどくさい
DAMO-YOLO
YOLOは・・・
YOLO
犬
一度AIを通すだけでクラス&物体位置を検出可能
You Only Look Once:一度見るだけ!
ちょこっとだけYOLOの仕組みを説明
DAMO-YOLO
アンカーボックスの考え方
YOLO
ゼロからボックスを予測するのはコストがかかる・・・(どんなボックスを書けばいいかわからん)
そこでアンカーボックス
アンカーボックス:データセットでよく出るボックスサイズを事前に教え込んでおく
例:車検出
事前知識として教える
YOLOは数種類のアンカーボックスを覚えている
がよく出てる
DAMO-YOLO
グリッドの考え方
各グリッドのマス目に対して
YOLO
物体の中心の有無を予測するよ
に物体の中心があると思うよ!
物体があるかどうかは分かったニャ
どうやって物体のサイズわかるニャ?
ここでアンカーボックスが生きてきます!
DAMO-YOLO
に物体があるとして、どのアンカーボックスを使うと検出できそうか
そしてちょっと修正
今回はこれが合いそう
が本当の中心
左上の座標からどれくらいずれているかを予測する必要
は完全に一致しない
立幅と横幅をいい感じに調整する必要
YOLOのイメージ
ざっくり予測してちょこっと修正
とりあえず動かしてみる
YOLOが若干分かったところで
まずはDAMO-YOLOで遊びましょう!
とりあえず動かしてみる
まずは環境構築をしましょう!
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
公式に従って
を実行し、環境を作りましょう
とりあえず動かしてみる
まずはPyTorchでリアルタイム推論をする
いろんなモデルを改造する際の勘所
これを実践してみましょう
とりあえず動かしてみる
つまり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行目から
とりあえず動かしてみる
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行目
とりあえず動かしてみる
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行目
とりあえず動かしてみる
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を作成
とりあえず動かしてみる
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行目
とりあえず動かしてみる
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の検出において
犬 score:0.87
Class:名称
score:その物体だと思ったときにどれくらい自信があるか
boundingBox:物体の境界線(左上の座標(xy),右下の座標(xy)の計4つで表しがち)
BBox・Class・Scoreはたびたび出るので
覚えておいてください!
コードで出てきたときになぜ必要か分かり易くなると思います
とりあえず動かしてみる
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に追記
とりあえず動かしてみる
リアルタイムに推論するには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行目)
とりあえず動かしてみる
遅い・・・・・
遅い要因を考える
学習や推論など汎用的にできる
モデルを柔軟に構築できる
推論に特化した構造になっていない
ONNXに変換することで推論の高速化を実現
あいまいな内容にも対応できるようになっているので
ONNX
モデルをいろいろなプラットフォームで実行できるようにするやつ
ONNXに変換するといいこと
モデルがコンパイルされることでCPUで高速に実行することができる
まずはONNXへ変換してみましょう
pip install onnx
pip install onnxruntime
あいまいさの排除
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を作成
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で推論してみましょう!
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
ONNXで推論
1/5 !?!?!?!?!?!
ONNXで推論
早くなったのはいいけど、
多重に検出するようになってしまった
後処理
Non-Maximum-Suppression
一番確率が高い
一番確率が高いものを
ピックアップ
それと重なり度合いが一定以上
のものは消す
bboxes, scores, cls_inds = nms_fast(bboxes, scores, cls_inds)
ONNXで推論
多重検出は消えた・・・
でも処理遅すぎん????
NMSは使い方注意
8400個全部入れたためめちゃ遅くなった
入れる個数を事前に減らす
ONNXで推論
bboxes = bboxes[scores > conf]
cls_inds = cls_inds[scores > conf]
scores = scores[scores > conf]
conf以上のものだけ取ってくる
絞る前:(8400, 4)
絞った後(9, 4)
この数なら高速にできるのでは!!
confより大きければTrue・そうでなければFalseが入っている
ONNXで推論
もう少し考える・・・
前処理と後処理をいちいち書くのめんどくさい・・・。
モデルに全部入れ込みたい・・・。
できるんです!
にはモデルを鬼カスタムする方法がある
最後に鬼カスタムして前処理・後処理を入れ込みましょう!
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)
いちいち変換するのめんどっちい
この処理も内部で出来んか???
これらを鬼カスタムモデルにぶち込みます!
鬼カスタムモデルの作り方
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
鬼カスタムモデルの作り方
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)
絞った後の検出数は可変
後ろのデータ部分は固定
鬼カスタムモデルの推論
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)