OpenCV 5.0.0
Open Source Computer Vision
読み込み中...
検索中...
見つかりません
🤖 AIによる機械翻訳(非公式) — これは OpenCV 5.0.0 公式リファレンス(英語)を AI (Claude) で自動翻訳したものです。訳に誤りを含む場合があります。正確な情報は 公式英語版(原文) を参照してください。
YOLO DNN

前のチュートリアル: OpenVINOでのOpenCV利用
次のチュートリアル: ブラウザでディープネットワークを実行する方法

原著者Alessandro de Oliveira Faria
拡張者Abduragim Shtanchaev
互換性OpenCV >= 4.9.0

OpenCVでの学習済みYOLOモデルの実行

学習済みモデルのデプロイは機械学習でよくあるタスクであり、特にPyTorchのような特定のフレームワークをサポートしないハードウェアを扱う場合に重要になる。本ガイドでは、PyTorchからYOLOファミリの学習済みモデルをエクスポートし、OpenCVのDNNフレームワークを使ってデプロイする方法を包括的に解説する。説明のために YOLOX モデルを中心に扱うが、この手法はサポートされている他のモデルにも適用できる。

覚え書き
Currently, OpenCV supports the following YOLO models:

このサポートには、これらのモデルに固有の前処理および後処理ルーチンが含まれる。OpenCVのDNNモジュールからDarknet形式のサポートが削除されたため、古いバージョンのYOLO (v1〜v3) はもはやサポートされていない。

YOLOXモデルの学習に成功したと仮定すると、次のステップはこのモデルをエクスポートしてOpenCVで実行することである。この処理を進める前に対処すべき重要な検討事項がいくつかある。これらの点を詳しく見ていこう。

YOLOの前処理と出力

YOLOファミリの検出器に関連する入力と出力の性質を理解することが極めて重要である。これらの検出器は、ほとんどのディープニューラルネットワーク (DNN) と同様に、モデルのスケールに応じて入力サイズが変化するのが一般的である。

モデルスケール入力サイズ
小規模モデル 1416x416
中規模モデル 2640x640
大規模モデル 31280x1280

この表は、さまざまなYOLOモデルの入力でよく使われる入力次元を素早く参照するためのものである。これらは標準的な入力形状である。学習に使ったモデルの入力サイズが表に記載されているサイズと異なる場合は、必ずその入力サイズを使用すること。

この処理の次に重要となるのは、YOLO検出器における画像の前処理の詳細を理解することである。基本的な前処理の方針はYOLOファミリ全体でほぼ共通だが、性能の低下を避けるために考慮すべき細かくも重要な違いがある。その中でも重要なのは、リサイズ後に適用される resize typepadding value である。例えば YOLOXモデルLetterBox のリサイズ方式とパディング値 114.0 を使用する。これらの引数を正規化定数とともに、エクスポートするモデルに適切に一致させることが不可欠である。

モデルの出力に関しては、通常 [BxNxC+5] または [BxNxC+4] の次元を持つテンソルの形を取る。ここで 'B' はバッチサイズ、'N' はアンカー数、'C' はクラス数(例えばモデルをCOCOデータセットで学習した場合は80クラス)を表す。前者のテンソル構造で追加される5は、オブジェクトネススコア (obj)、信頼度スコア (conf)、およびバウンディングボックス座標 (cx, cy, w, h) に対応する。注目すべき点として、YOLOv8モデルの出力は [BxNxC+4] の形を取り、明示的なオブジェクトネススコアが存在せず、オブジェクトスコアはクラススコアから直接推定される。特にYOLOXモデルでは、予測を画像領域に再スケールするためにアンカーポイントを組み込む必要もある。このステップはONNXグラフに統合されることになり、その処理については後続のセクションでさらに詳しく説明する。

PyTorchモデルのエクスポート

前処理の引数が分かったので、次に進んでモデルをPytorchからONNXグラフへエクスポートできる。本チュートリアルではサンプルモデルとしてYOLOXを使用しているため、説明のためにそのエクスポートを使う(YOLOv10 モデルを除き、他のYOLO検出器でも処理は同じである。YOLOv10のエクスポート方法については本記事の後半で詳しく述べる)。YOLOXをエクスポートするには エクスポートスクリプト をそのまま使えばよい。具体的には次のコマンドが必要である:

git clone https://github.com/Megvii-BaseDetection/YOLOX.git
cd YOLOX
wget https://github.com/Megvii-BaseDetection/YOLOX/releases/download/0.1.1rc0/yolox_s.pth # download pre-trained weights
python3 -m tools.export_onnx --output-name yolox_s.onnx -n yolox-s -c yolox_s.pth --decode_in_inference

注意: ここで --decode_in_inference は、アンカーボックスの生成をONNXグラフ自体に含めるためのものである。これは この値True に設定し、その結果アンカー生成関数が含まれる。

以下では、必要な場合に備えて、エクスポートスクリプトの最小版(YOLOX以外のモデルにも使える)を示す。ただし通常は、各YOLOリポジトリにあらかじめ定義されたエクスポートスクリプトが用意されている。

import onnx
import torch
from onnxsim import simplify
# load the model state dict
ckpt = torch.load(ckpt_file, map_location="cpu")
model.load_state_dict(ckpt)
# prepare dummy input
dummy_input = torch.randn(args.batch_size, 3, exp.test_size[0], exp.test_size[1])
#export the model
torch.onnx._export(
model,
dummy_input,
"yolox.onnx",
input_names=["input"],
output_names=["output"],
dynamic_axes={"input": {0: 'batch'},
"output": {0: 'batch'}})
# use onnx-simplifier to reduce reduent model.
onnx_model = onnx.load(args.output_name)
model_simp, check = simplify(onnx_model)
assert check, "Simplified ONNX model could not be validated"
onnx.save(model_simp, args.output_name)

YOLOv10モデルのエクスポート

YOLOv10を実行するには、torchから動的形状を伴う後処理を切り離してからONNXに変換する必要がある。後処理の切り離し方法を探している場合は、公式YOLOv10から派生したこの フォークブランチ がある。このフォークブランチは、後処理手順自体の前にモデルの 出力を返す ことで後処理を切り離している。torchモデルをONNXに変換するには、この手順に従う。

git clone git@github.com:Abdurrahheem/yolov10.git
conda create -n yolov10 python=3.9
conda activate yolov10
pip install -r requirements.txt
python export_opencv.py --model=<model-name> --imgsz=<input-img-size>

デフォルトでは --model="yolov10s" および --imgsz=(480,640) である。これにより yolov10s.onnx ファイルが生成され、OpenCVでの推論に使用できる

OpenCVサンプルでのYolo ONNX検出器の実行

モデルのONNXグラフが用意できたら、OpenCVのサンプルでそのまま実行できる。そのためには次の点を確認する必要がある:

  1. OpenCVが -DBUILD_EXAMLES=ON フラグ付きでビルドされていること。
  2. OpenCVの build ディレクトリに移動する
  3. 次のコマンドを実行する:
./bin/example_dnn_object_detection <model_name> --input=<path_to_your_input_file> \
--labels=<path_to_class_names_file> \
--thr=<confidence_threshold> \
--nms=<non_maximum_suppression_threshold> \
--mean=<mean_normalization_value> \
--scale=<scale_factor> \
--padvalue=<padding_value> \
--paddingmode=<padding_mode> \
--backend=<computation_backend> \
--target=<target_computation_device> \
--width=<model_input_width> \
--height=<model_input_height> \
  • –input: 入力画像または動画のファイルパス。省略した場合はカメラからフレームを取得する。
  • –labels: 物体検出用のクラス名を含むテキストファイルへのファイルパス。
  • –thr: 検出の信頼度しきい値(例: 0.5)。
  • –nms: Non-maximum suppression のしきい値(例: 0.4)。
  • –mean: 平均正規化の値(例: 平均正規化を行わない場合は 0.0)。
  • –scale: 入力正規化のスケール係数(例: 1.0, 1/255.0 など)。
  • –yolo: YOLOモデルのバージョン (例: YOLOv8, YOLOX, YOLOv10 など)。
  • –padvalue: 前処理で使用するパディング値(例: 114.0)。
  • –paddingmode: 画像のリサイズとパディングの処理方法。選択肢: 0(追加処理なしでリサイズ)、1(リサイズ後にクロップ)、2(アスペクト比を保持してリサイズ)。
  • –backend: 計算バックエンドの選択(0は自動、1はHalide、2はOpenVINO など)。
  • –target: 計算対象デバイスの選択(0はCPU、1はOpenCL など)。
  • –device: カメラデバイス番号(0はデフォルトカメラ)。--input が指定されない場合はインデックス0のカメラがデフォルトで使用される。
  • –width: モデルの入力幅。画像の幅と混同しないこと。(例: 416, 480, 640, 1280 など)。
  • –height: モデルの入力高さ。画像の高さと混同しないこと。(例: 416, 480, 640, 1280 など)。

ここで mean, scale, padvalue, paddingmode は、モデルがPyTorchでの結果と一致するように、前処理セクションで説明したものと厳密に一致させる必要がある

自分で用意した学習済みモデルなしでOpenCVのYOLOサンプルを実行する方法を示すため、次の手順に従う:

  1. プラットフォームにPythonがインストールされていることを確認する。
  2. OpenCVが -DBUILD_EXAMPLES=ON フラグ付きでビルドされていることを確認する。

YOLOX検出器を実行する(デフォルト値で):

cd opencv/samples/dnn
export OPENCV_DOWNLOAD_CACHE_DIR=<path to download the model>
cd ../data
export OPENCV_SAMPLES_DATA_PATH=$(pwd)
python download_models.py yolov8x --save_dir=$OPENCV_DOWNLOAD_CACHE_DIR
cd <build directory of OpenCV>
./bin/example_dnn_object_detection yolov8x

これによりカメラを使ってYOLOX検出器が実行される。YOLOv8(例として)の場合は、次の追加手順に従う:

cd opencv/samples/dnn
export OPENCV_DOWNLOAD_CACHE_DIR=<path to download the model>
cd ../data
export OPENCV_SAMPLES_DATA_PATH=$(pwd)
python download_models.py yolov8n --save_dir=$OPENCV_DOWNLOAD_CACHE_DIR
cd <build directory of OpenCV>
./bin/example_dnn_object_detection yolov8n --model=onnx/models/yolov8n.onnx --mean=0.0 --scale=0.003921568627 --paddingmode=2 --padvalue=144.0 --thr=0.5 --nms=0.4 --rgb=0

動画デモ:

カスタムパイプラインの構築

推論パイプラインに独自の調整を加える必要がある場合もある。OpenCV DNNモジュールを使えば、これもかなり簡単に実現できる。以下では、サンプルの実装の詳細を概説する:

  • 必要なライブラリをインポートする
#include <fstream>
#include <sstream>
#include <opencv2/dnn.hpp>
#include <mutex>
#include <thread>
#include <queue>
#include "iostream"
#include "common.hpp"
  • ONNXグラフを読み込み、ニューラルネットワークモデルを作成する:
if ((parser.get<String>("backend") != "default") || (parser.get<String>("target") != "cpu")){
engine = ENGINE_CLASSIC;
}
Net net = readNet(modelPath, configPath, "", engine);
int backend = getBackendID(parser.get<String>("backend"));
net.setPreferableBackend(backend);
net.setPreferableTarget(getTargetID(parser.get<String>("target")));
net.setProfilingMode(DNN_PROFILE_SUMMARY);
  • 画像を読み込んで前処理する:
scale = parser.get<float>("scale");
meanv = parser.get<Scalar>("mean");
swapRB = parser.get<bool>("rgb");
inpWidth = parser.get<int>("width");
inpHeight = parser.get<int>("height");
int async = parser.get<int>("async");
paddingValue = parser.get<float>("padvalue");
const string postprocessing = parser.get<String>("postprocessing");
paddingMode = static_cast<ImagePaddingMode>(parser.get<int>("paddingmode"));
Image2BlobParams imgParams(
Scalar::all(scale),
size,
meanv,
swapRB,
DNN_LAYOUT_NCHW,
paddingMode,
paddingValue);
inp = blobFromImageWithParams(frame, imgParams);
  • 推論:
vector<int> keep_classIds;
vector<float> keep_confidences;
vector<Rect2d> keep_boxes;
vector<Mat> outs;
net.forward(outs, net.getUnconnectedOutLayersNames());
net.printPerfProfile();
predictionsQueue.push(outs);
  • 後処理

すべての後処理ステップは関数 yoloPostProcess に実装されている。NMSステップはonnxグラフに含まれていないことに注意すること。サンプルではそれにOpenCVの関数を使用している。

yoloPostProcessing(outs, keep_classIds, keep_confidences, keep_boxes, confThreshold, nmsThreshold, postprocessing);
  • 予測されたボックスを描画する
int imgWidth = max(frame.rows, frame.cols);
int size = (stdSize*imgWidth)/stdImgSize;
int weight = (stdWeight*imgWidth)/stdImgSize;
int thickness = (stdThickness*imgWidth)/stdImgSize;
for (size_t idx = 0; idx < boxes.size(); ++idx){
Scalar boxColor = getColor(classIds[idx]);
int left = boxes[idx].x;
int top = boxes[idx].y;
int right = boxes[idx].x + boxes[idx].width;
int bottom = boxes[idx].y + boxes[idx].height;
rectangle(frame, Point(left, top), Point(right, bottom), boxColor, thickness);
string label = format("%.2f", confidences[idx]);
if (!labels.empty())
{
CV_Assert(classIds[idx] < (int)labels.size());
label = labels[classIds[idx]] + ": " + label;
}
Rect r = getTextSize(Size(), label, Point(), sans, size, weight);
int baseline = r.y + r.height;
Size labelSize = Size(r.width, r.height + size/4 - baseline);
top = max(top-thickness/2, labelSize.height);
rectangle(frame, Point(left-thickness/2, top-(labelSize.height)),
Point(left + labelSize.width, top), boxColor, FILLED);
putText(frame, label, Point(left, top-size/4), getTextColor(boxColor), sans, size, weight);
}