LoginSignup
12

More than 1 year has passed since last update.

C++でONNXRuntimeをビルドして推論するまで

Last updated at Posted at 2022-01-09

はじめに

C++でDNNの推論を行う時のライブラリとして、Pythonで学習を行った時のフレームワーク(PyTorchTensorFlow)のC++APIをそのまま使う手もありますが、それ以外にONNXRuntimeが有力な候補として上げられます。
筆者の所感ですが、PyTorchやTensorFlowのC++APIと比べると

  • バイナリサイズが小さい
  • 静的ライブラリのビルド、リンクが比較的簡単
  • 実行速度が同等かそれ以上

といったメリットがあります。

しかし、ビルドから実際に使用するところまでの情報が散逸しており、実際に動かしてみるまで苦労したので、この記事で一通りの流れをまとめます。

この記事でやること

ONNXRuntimeをソースからビルド

ONNXRuntimeのC++ライブラリをソースからビルドし、推論アプリケーションで使うために必要なライブラリ群を列挙します。
ONNXRuntimeライブラリを静的リンクしたい場合、必要なライブラリが複数箇所に散らばっているため注意が必要です。

ONNXフォーマットのモデルを読み込んで推論を行うC++アプリケーションの例

ONNXフォーマットのモデルの読み込みから推論までを行うコードをC++で書きます。
今回の例では推論を行うDNNモデルとしてResNet50を使用します。
pythonでPyTorchからONNXフォーマットに変換しますが、変換元はPyTorchに限らずTensorFlowや他のフレームワークのモデルでもOKです。

上記アプリケーションのビルド

ONNXRuntimeライブラリをリンクして推論を行う上記アプリケーションをビルドするため、CMakeLists.txtを書きます。

この記事でやらないこと

ONNXRuntimeのEP利用

ONNXRuntimeはそのままでも使用できますが、NVIDIA CUDAやIntel oneDNN等の様々な外部ライブラリを用いることでハードウェアアクセラレーション等の恩恵を受けることができます。
このような外部ライブラリをExecution Provider(EP)と呼びます。
EPの利用にはメリットがある反面、作業工程が増えるため、この記事では扱いません。

Execution Providers - onnxruntime
Build with different EPs - onnxruntime

ONNXRuntimeの高速化

上記EP利用と同様に、ONNXRuntimeを高速化するための設定について、この記事では扱いません。

Performance - onnxruntime

ONNXRuntimeを使ったDNNの学習

v1.8.0からONNXRuntimeを使ってDNNの学習を行うことが可能になりました。
しかし、この記事では推論を行うための流れをまとめるため、学習機能は扱いません。

ORT Training with PyTorch - onnxruntime
Build for training - onnxruntime

Linux/MacOS対応

筆者の環境がWindowsのみのため、この記事内のコードはWindowsを前提として書いています。
実際にはCMakeを使い、OS依存の部分を可能な限り排除しているため、Linux/MacOSでも流れの確認は可能ですが、実際の動作は保証しません。
Linux/MacOSユーザーの方は適宜改変してください。また、記事内の単語についても適宜読み替えてください。

環境

この記事ではWindows10とVisual Studio 2019を前提とします。

ONNXRuntimeのビルドに必要なもの

  • CMake >= 3.18
  • Python >= 3.6
  • Visual Studio >= 2017

ONNXフォーマットのモデルの出力に必要なもの

  • Python3
  • PyTorch
  • Numpy
  • Pillow

推論アプリケーションのビルドに必要なもの

  • CMake >= 3.15
  • OpenCV

OpenCVは画像の読み込みとfloatへの変換に使用しています。
同等の機能があれば他のライブラリでも大丈夫です。

筆者の環境

参考のため筆者の環境を以下に記します。

  • Windows10 21H2

C++

  • Visual Studio 2019
  • CMake 3.19.1
  • OpenCV 4.5.5

Python

  • Miniconda 4.9.2
  • Python 3.8.12
  • PyTorch 1.10.1
  • Numpy 1.20.3
  • Pillow 8.4.0

ONNXRuntimeのビルド

まずはONNXRuntimeをC++でビルドします。

Build for inferencing - onnxruntime

クローン

ONNXRuntimeをgithubからクローンしましょう。

>git clone https://github.com/microsoft/onnxruntime.git
>cd onnxruntime

適当なバージョンでチェックアウトしておきます。

>git checkout refs/tags/v1.10.0

ビルドの実行

ビルド用のファイルがあるので、これを実行します。
CMakeとPythonにパスが通っていること(cmake, pythonコマンドが実行可能なこと)が必要なので、注意してください。
また、Debug版で7.5GBほど必要なのでこちらも注意です。

>build.bat --config Release --build_shared_lib --parallel

Linux/MacOSの場合はbuild.batbuild.shで置き換えてください。

-hオプションでオプションの一覧を見ることができます。
batファイル自体はpythonを実行するためのラッパーになっており、実際に実行されるのはtools/ci_build/build.py(github)です。
オプションの処理にはpythonのArgumentParserを使用しているので、上記pythonファイルからもオプションを確認できます。

オプションの数が膨大なので、よく使うものを抜粋しておきます。

コマンド 説明
--config CONFIG Debug, MinSizeRel, RelWithDebInfo, Releaseのいずれか
--build_shared_lib 共有ライブラリをビルドするかどうかのフラグ。これをつけても静的ライブラリはビルドされるので、とりあえずつけておいてOK。
--skip_tests ビルド後のテストをスキップするかどうかのフラグ
--parallel NUM_JOBS 並列ビルドを行う。NUM_JOBSを指定しない場合はCPUコア数から自動的に決定される。
--cmake_generator GENERATOR CMakeで使用するGeneratorを指定する。Visual Studio 15 2017, Visual Studio 16 2019(デフォルト), Visual Studio 17 2022, Ninjaのいずれか。 Windowsのみ有効。
--enable_lto リンク時最適化を有効化するフラグ
--enable_msvc_static_runtime MSVCランタイムライブラリをMTd(Debug版)もしくはMT(それ以外)に指定するフラグ。推論アプリケーションのビルド設定と一致させる必要がある。

--parallelオプションは基本的にジョブ数を指定せず使用して大丈夫ですが、メモリ不足によってビルドが失敗する場合、CPUコア数より少ない数で直接指定してください。

マシンスペック次第ですが、手元のRyzen5 2500UのノートPCでは、Debug版のビルドにテスト含めて30分ほどかかりました。
コーヒーでも飲んで気長に待ちましょう。

生成物の確認

ビルドが終わるとbuild/Windows/[CONFIG]フォルダに諸々のファイルが生成されています。([CONFIG]Debug等で置き換えてください。)
これらのうち、アプリケーションのビルドに必要なものを列挙します。
適当なフォルダにコピーしておくと後で楽になります。

共有ライブラリ

ファイル 場所 (build/Windows/[CONFIG]以下) デバッグ情報(pdbファイル)の場所
onnxruntime.dll
onnxruntime.lib
[CONFIG] [CONFIG]

静的ライブラリ

依存ライブラリが各所に散らばっているので、漏れがないように注意しましょう。
Debug版、RelWithDebInfo版の場合はデバッグ情報(pdbファイル)も忘れずに。

ファイル 場所 (build/Windows/[CONFIG]以下) デバッグ情報(pdbファイル)の場所
onnxruntime_session.lib
onnxruntime_optimizer.lib
onnxruntime_providers.lib
onnxruntime_util.lib
onnxruntime_framework.lib
onnxruntime_graph.lib
onnxruntime_mlas.lib
onnxruntime_common.lib
onnxruntime_flatbuffers.lib
[CONFIG] onnxruntime_*.dir/[CONFIG]
onnx.lib
onnx_proto.lib
external/onnx/[CONFIG] external/onnx/onnx.dir/[CONFIG]
external/onnx/onnx_proto.dir/[CONFIG]
libprotobuf-lite.lib
(or libprotobuf-lited.lib)
external/protobuf/cmake/[CONFIG] external/protobuf/cmake/libprotobuf-lite.dir/[CONFIG]
re2.lib external/re2/[CONFIG] external/re2/re2.dir/[CONFIG]
flatbuffers.lib external/flatbuffers/[CONFIG] external/flatbuffers/flatbuffers.dir/[CONFIG]
cpuinfo.lib external/pytorch_cpuinfo/[CONFIG] external/pytorch_cpuinfo/cpuinfo.dir/[CONFIG]
clog.lib external/pytorch_cpuinfo/deps/clog/[CONFIG] external/pytorch_cpuinfo/deps/clog/clog.dir/[CONFIG]

onnxruntime_providers.libと似た名前のonnxruntime_providers_shared.libがありますが、こちらはEP利用のためのライブラリなので、デフォルトでは必要ありません。

推論アプリケーションの作成

ビルドしたONNXRuntimeライブラリ群を使って、実際にResNet50の推論を行うアプリケーションを作成してみます。

ONNXフォーマットのResNet50モデルの生成

C++コードを書く前に、ONNXフォーマットのResNet50モデルを用意しておきましょう。

ResNet | PyTorch
torch.onnx — PyTorch 1.10.1 documentation

PyTorchモデルから変換

今回はPyTorchのResNet50学習済みモデルを使用します。
PyTorchのモデルは、画像のピクセル値を[0, 255]から[0, 1]に変換し、さらに正規化したものを入力しなければなりません。
今回は、[0, 255]から[0, 1]への変換はC++側で行い、正規化はモデルに含めるようにしました。
また、出力は確率ではなくsoftmaxを通す前の値なので、softmaxもモデルに含めるようにしました。

import torch

def get_pytorch_model():
    # ResNet50の学習済みモデルをロード
    model = torch.hub.load('pytorch/vision:v0.10.0', 'resnet50', pretrained=True)

    # 正規化処理とSoftmaxをモデルに連結して新しいモデルを作成
    return torch.nn.Sequential(
        torchvision.transforms.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225]),
        model,
        torch.nn.Softmax(-1)
    )

変換処理は以下のようになります。
入力の画像サイズは224x224で固定としました。
入力と出力の名前(input_names, output_namesに指定した文字列)はC++側で推論を行う際に必要なので、覚えておきましょう。

import os
import torchvision

def save_model(output_dir: str):
    # 変換するモデルを取得
    model = get_pytorch_model()

    # ダミーの入力
    dummy_input = torch.randn(1, 3, 224, 224)

    # ONNXフォーマットに変換して出力
    filename = os.path.join(output_dir, 'resnet50.onnx')
    torch.onnx.export(
        model,
        dummy_input,
        filename,
        input_names=['input0'],
        output_names=['output0'],
        dynamic_axes={'input0': {0: 'batch_size'}}
    )

PyTorchのResNet50学習済みモデルのページに従い、保存したモデルの出力ラベルも保存しておきます。

def save_imagenet_label(output_dir: str):
    filename = os.path.join(output_dir, 'imagenet_classes.txt')
    torch.hub.download_url_to_file(
        'https://raw.githubusercontent.com/pytorch/hub/master/imagenet_classes.txt', filename)

同様に、推論アプリケーションのテストに用いる画像も保存しておきます。
ダウンロードした画像はサイズが合わないので、224x224に変換した画像もあらかじめ作成しました。

from PIL import Image

def save_image_sample(output_dir: str):
    # 画像をダウンロード
    filename = os.path.join(output_dir, 'dog.jpg')
    torch.hub.download_url_to_file(
        'https://github.com/pytorch/hub/raw/master/images/dog.jpg', filename)

    # 224x224サイズに編集して別途保存
    image = Image.open(filename)
    image = torchvision.transforms.functional.resize(image, size=256)
    image = torchvision.transforms.functional.center_crop(image, output_size=224)
    image.save(os.path.join(output_dir, 'dog_input.png'))

全体は以下のようになります。

クリックで展開
output_resource.py
import os
import argparse

import torch
import torchvision
from PIL import Image


def save_image_sample(output_dir: str):
    # 画像をダウンロード
    filename = os.path.join(output_dir, 'dog.jpg')
    torch.hub.download_url_to_file(
        'https://github.com/pytorch/hub/raw/master/images/dog.jpg', filename)

    # 224x224サイズに編集して別途保存
    image = Image.open(filename)
    image = torchvision.transforms.functional.resize(image, size=256)
    image = torchvision.transforms.functional.center_crop(image, output_size=224)
    image.save(os.path.join(output_dir, 'dog_center.jpg'))


def save_imagenet_label(output_dir: str):
    filename = os.path.join(output_dir, 'imagenet_classes.txt')
    torch.hub.download_url_to_file(
        'https://raw.githubusercontent.com/pytorch/hub/master/imagenet_classes.txt', filename)


def get_pytorch_model():
    # ResNet50の学習済みモデルをロード
    model = torch.hub.load('pytorch/vision:v0.10.0', 'resnet50', pretrained=True)

    # 正規化処理とSoftmaxをモデルに連結して新しいモデルを作成
    return torch.nn.Sequential(
        torchvision.transforms.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225]),
        model,
        torch.nn.Softmax(-1)
    )


def save_model(output_dir: str):
    # 変換するモデルを取得
    model = get_pytorch_model()

    # ダミーの入力
    dummy_input = torch.randn(1, 3, 224, 224)

    # ONNXフォーマットに変換して出力
    filename = os.path.join(output_dir, 'resnet50.onnx')
    torch.onnx.export(
        model,
        dummy_input,
        filename,
        input_names=['input0'],
        output_names=['output0'],
        dynamic_axes={'input0': {0: 'batch_size'}}
    )


if __name__ == '__main__':
    parser = argparse.ArgumentParser()
    parser.add_argument('-o', '--output-dir', type=str, required=True)
    args = parser.parse_args()

    output_dir = os.path.normpath(args.output_dir)
    if not os.path.isabs(output_dir):
        output_dir = os.path.join(os.getcwd(), output_dir)

    if not os.path.exists(output_dir):
        os.makedirs(output_dir)

    save_image_sample(output_dir)
    save_imagenet_label(output_dir)
    save_model(output_dir)

PyTorchモデルの推論結果を確認

C++アプリケーションの結果と比較できるように、PyTorchモデルの推論結果を確認しておきましょう。

check_inference_result.py
import argparse
import os
import numpy as np
from PIL import Image
import torch
import torchvision

from output_resource import get_pytorch_model

if __name__ == '__main__':
    parser = argparse.ArgumentParser()
    parser.add_argument('-i', '--input', type=str, required=True)
    parser.add_argument('-l', '--label', type=str, required=True)
    args = parser.parse_args()

    image_filename = os.path.normpath(args.input)
    label_filename = os.path.normpath(args.label)

    # 画像の読み込み
    assert os.path.exists(image_filename)
    image = Image.open(image_filename)

    assert image.height == 224 and image.width == 224

    # ラベルの読み込み
    with open(label_filename, mode='r') as f:
        labels = [s.strip() for s in f.readlines()]

    # モデルの読み込み
    model = get_pytorch_model()
    model.eval()

    # 画像の前処理
    input_tensor = torchvision.transforms.functional.to_tensor(image)  # [0,1]に変換
    input_tensor = input_tensor.unsqueeze(0)  # CHW -> BCHW

    # 推論を実行
    with torch.no_grad():
        results = model(input_tensor)
        result = results[0].numpy().copy()

    # Top5を表示
    indices = np.argsort(result)[::-1]
    for i in range(5):
        index = indices[i]
        print(f'{i + 1}: {labels[index]} {result[index]}')

実際に実行してみます。

>python python/check_inference_result.py -i resource/dog_input.png -l resource/imagenet_classes.txt
1: Samoyed 0.8732958436012268
2: Pomeranian 0.030270852148532867
3: white wolf 0.019671261310577393
4: keeshond 0.011073519475758076
5: Eskimo dog 0.009204263798892498

Samoyedが約87%となり、うまく推論できていそうです。

サモエド - Wikipedia

OpenCVの導入

今回は画像読み込みにOpenCVを使用します。導入していない場合は先に入れておきましょう。
Windowsの場合は公式HPのリリースまたはgithubからダウンロードして、適当な箇所で展開しておきます。
opencv_world*.dllがあるフォルダにパスを通しておくと後で楽です。
Macはhomebrewで導入できるはず。
Linuxはソースからビルドする感じかな? ググれば情報見つかると思うので割愛します。

C++で推論アプリケーションのコードを書く

必要なファイルが揃ったところで、C++で推論アプリケーションのコードを書いていきましょう。

基本的な情報はONNXRuntime公式のサンプルコードを参考にしています。
(サンプルコードと言いつつ、WindowsOnlyなコードな上に余計な処理が多くて、もっと何とかならんの…?って思ってしまいました…)

画像・ラベルデータ読み込み

最初に画像とラベルデータを読み込む関数を定義しておきましょう。
画像読み込みにOpenCVを使っていますが、他のライブラリを使っても構いません。
本質からは外れるので折り畳んでいます。

クリックで展開
std::vector<float> loadImage(const std::string& filename)
{
    auto image = cv::imread(filename);

    // OpenCVはBGRの順で読み込むのでRGBに変換
    cv::cvtColor(image, image, cv::COLOR_BGR2RGB);

    // 1次元に変更
    image = image.reshape(1, 1);

    // [0, 255]のuint_8から[0, 1]のfloatに変換
    std::vector<float> vec;
    image.convertTo(vec, CV_32FC1, 1. / 255);

    // HWC -> CHW
    std::vector<float> output;
    for (size_t ch = 0; ch < 3; ++ch) {
        for (size_t i = ch; i < vec.size(); i += 3) {
            output.emplace_back(vec[i]);
        }
    }
    return output;
}

std::vector<std::string> loadLabels(const std::string& filename)
{
    std::vector<std::string> output;

    std::ifstream file(filename);
    if (file) {
        std::string s;
        while (std::getline(file, s)) {
            output.emplace_back(s);
        }
        file.close();
    }

    return output;
}

モデル読み込み

まずはモデル読み込み部分です。
細かくチューニングするのでなければ簡単です。
モデルの読み込みに失敗した場合等、Ort::SessionのコンストラクタがOrt::Exceptionを投げる可能性があるので、万全を期すならばエラー処理しましょう。

const std::wstring modelFile = L"...";

Ort::Env env;
Ort::SessionOptions sessionOptions;

Ort::Session session(nullptr);
try {
    session = Ort::Session(env, modelFile.data(), sessionOptions);
}
catch(Ort::Exception& e) {
    std::cout << e.what() << std::endl;
    return 1;
}

入出力周りの準備

次に入出力用の配列を確保します。
予め要素数が分かっているのでstd::arrayを使っていますが、std::vectorでもOKです。(配列として扱えればOK)

constexpr int64_t numChannels = 3;
constexpr int64_t width = 224;
constexpr int64_t height = 224;
constexpr int64_t numClasses = 1000;
constexpr int64_t numInputElements = numChannels * height * width;

// 入出力のshape
const std::array<int64_t, 4> inputShape = { 1, numChannels, height, width };
const std::array<int64_t, 2> outputShape = { 1, numClasses };

// 入出力用の配列
std::array<float, numInputElements> input;
std::array<float, numClasses> results;

実際に推論を行うためには入出力用配列からTensorを作成する必要があります。
作成したTensorは入出力用配列のポインタを保持するので、入出力用配列を削除しないように注意です。
また、std::vectorを使う場合は以後メモリの再確保を行わないようにしましょう。

// 入出力用のTensor
// 内部で配列の先頭ポインタを保持する
auto memory_info = Ort::MemoryInfo::CreateCpu(OrtDeviceAllocator, OrtMemTypeCPU);
auto inputTensor = Ort::Value::CreateTensor<float>(memory_info, input.data(), input.size(), inputShape.data(), inputShape.size());
auto outputTensor = Ort::Value::CreateTensor<float>(memory_info, results.data(), results.size(), outputShape.data(), outputShape.size());

予め入出力配列を用意せずにTensorを作成することもできますが、std::arraystd::vectorで保持した方が何かと便利です。

実際のデータの読み書きはTensorではなく、入出力配列から行うことができます。
繰り返し推論を実行する際も、Tensorを何度も作成する必要はなく、入出力配列を弄るだけでOKです。

const std::string imageFile = "...";
const std::vector<float> imageVec = loadImage(imageFile); // CHW形式を1次元にした画像データ
assert(imageVec.size() == numInputElements);

// 画像データを入力用配列にコピー
std::copy(imageVec.begin(), imageVec.end(), input.begin());

最後に入出力の名前を設定します。
ONNXフォーマットに出力した際に指定したものを同じ名前にしましょう。
モデルデータから取得することもできますが、入出力が複数ある場合は順番に注意が必要です。

// 入出力の名前
// ONNXモデルの保存時と同じ名前にすること
const std::array<const char*, 1> inputNames = { "input0" };
const std::array<const char*, 1> outputNames = { "output0" };

/*
// 読み込んだモデルデータから入出力名を取得することもできる
// 入出力が複数ある場合は順番に注意
std::vector<const char*> inputNames;
std::vector<const char*> outputNames;

Ort::AllocatorWithDefaultOptions alloc;
for (size_t i = 0; i < session.GetInputCount(); ++i) {
    inputNames.emplace_back(session.GetInputName(i, alloc));
}
for (size_t i = 0; i < session.GetOutputCount(); ++i) {
    outputNames.emplace_back(session.GetOutputName(i, alloc));
}
*/

推論の実行

ここまでで準備は終わりです。推論を実行しましょう。
結果を確認する処理も合わせて書いておきます。

// 推論を実行
Ort::RunOptions runOptions;
try {
    session.Run(runOptions, inputNames.data(), &inputTensor, 1, outputNames.data(), &outputTensor, 1);
}
catch (Ort::Exception& e) {
    std::cout << e.what() << std::endl;
    return 1;
}

// 結果から順位付け
std::vector<std::pair<size_t, float>> indexValuePairs;
for (size_t i = 0; i < results.size(); ++i) {
    indexValuePairs.emplace_back(i, results[i]);
}
std::sort(indexValuePairs.begin(), indexValuePairs.end(), [](const auto& lhs, const auto& rhs) { return lhs.second > rhs.second; });

// Top5を表示
const std::string labelFile = "...";
const std::vector<std::string> labels = loadLabels(labelFile); // 出力に対応したラベル
for (size_t i = 0; i < 5; ++i) {
    const auto& [index, value] = indexValuePairs[i];
    std::cout << i + 1 << ": " << labels[index] << " " << value << std::endl;
}

コード全体

以上のコードをまとめると以下のようになります。

クリックで展開
main.cpp
#include <fstream>
#include <iostream>
#include <array>
#include <string>

#include <opencv2/core/mat.hpp>
#include <opencv2/imgcodecs.hpp>
#include <opencv2/imgproc.hpp>
#include <onnxruntime/core/session/experimental_onnxruntime_cxx_api.h>

std::vector<float> loadImage(const std::string& filename)
{
    auto image = cv::imread(filename);

    // OpenCVはBGRの順で読み込むのでRGBに変換
    cv::cvtColor(image, image, cv::COLOR_BGR2RGB);

    // 1次元に変更
    image = image.reshape(1, 1);

    // [0, 255]のuint_8から[0, 1]のfloatに変換
    std::vector<float> vec;
    image.convertTo(vec, CV_32FC1, 1. / 255);

    // HWC -> CHW
    std::vector<float> output;
    for (size_t ch = 0; ch < 3; ++ch) {
        for (size_t i = ch; i < vec.size(); i += 3) {
            output.emplace_back(vec[i]);
        }
    }
    return output;
}

std::vector<std::string> loadLabels(const std::string& filename)
{
    std::vector<std::string> output;

    std::ifstream file(filename);
    if (file) {
        std::string s;
        while (std::getline(file, s)) {
            output.emplace_back(s);
        }
        file.close();
    }

    return output;
}

int main()
{
    const std::wstring modelFile = L"...";

    Ort::Env env;
    Ort::SessionOptions sessionOptions;

    Ort::Session session(nullptr);
    try {
        session = Ort::Session(env, modelFile.data(), sessionOptions);
    }
    catch (Ort::Exception& e) {
        std::cout << e.what() << std::endl;
        return 1;
    }

    constexpr int64_t numChannels = 3;
    constexpr int64_t width = 224;
    constexpr int64_t height = 224;
    constexpr int64_t numClasses = 1000;
    constexpr int64_t numInputElements = numChannels * height * width;

    // 入出力のshape
    const std::array<int64_t, 4> inputShape = { 1, numChannels, height, width };
    const std::array<int64_t, 2> outputShape = { 1, numClasses };

    // 入出力用の配列
    std::array<float, numInputElements> input;
    std::array<float, numClasses> results;

    // 入出力用のTensor
    // 内部で配列の先頭ポインタを保持する
    auto memory_info = Ort::MemoryInfo::CreateCpu(OrtDeviceAllocator, OrtMemTypeCPU);
    auto inputTensor = Ort::Value::CreateTensor<float>(memory_info, input.data(), input.size(), inputShape.data(), inputShape.size());
    auto outputTensor = Ort::Value::CreateTensor<float>(memory_info, results.data(), results.size(), outputShape.data(), outputShape.size());

    // 画像データ読み込み
    const std::string imageFile = "...";
    const std::vector<float> imageVec = loadImage(imageFile); // CHW形式を1次元にした画像データ
    assert(imageVec.size() == numInputElements);

    // 画像データを入力用配列にコピー
    std::copy(imageVec.begin(), imageVec.end(), input.begin());

    // 入出力の名前
    // ONNXモデルの保存時と同じ名前にすること
    const std::array<const char*, 1> inputNames = { "input0" };
    const std::array<const char*, 1> outputNames = { "output0" };

    /*
    // 読み込んだモデルデータから入出力名を取得することもできる
    // 入出力が複数ある場合は順番に注意
    std::vector<const char*> inputNames;
    std::vector<const char*> outputNames;

    Ort::AllocatorWithDefaultOptions alloc;
    for (size_t i = 0; i < session.GetInputCount(); ++i) {
        inputNames.emplace_back(session.GetInputName(i, alloc));
    }
    for (size_t i = 0; i < session.GetOutputCount(); ++i) {
        outputNames.emplace_back(session.GetOutputName(i, alloc));
    }
    */

    // 推論を実行
    Ort::RunOptions runOptions;
    try {
        session.Run(runOptions, inputNames.data(), &inputTensor, 1, outputNames.data(), &outputTensor, 1);
    }
    catch (Ort::Exception& e) {
        std::cout << e.what() << std::endl;
        return 1;
    }

    // 結果から順位付け
    std::vector<std::pair<size_t, float>> indexValuePairs;
    for (size_t i = 0; i < results.size(); ++i) {
        indexValuePairs.emplace_back(i, results[i]);
    }
    std::sort(indexValuePairs.begin(), indexValuePairs.end(), [](const auto& lhs, const auto& rhs) { return lhs.second > rhs.second; });

    // Top5を表示
    const std::string labelFile = "...";
    const std::vector<std::string> labels = loadLabels(labelFile); // 出力に対応したラベル
    for (size_t i = 0; i < 5; ++i) {
        const auto& [index, value] = indexValuePairs[i];
        std::cout << i + 1 << ": " << labels[index] << " " << value << std::endl;
    }
}

CMakeLists.txtを書く

コードが書けたので、ビルドするためのCMakeLists.txtを書いていきます。

ONNXRuntimeを動的リンクする場合と静的リンクする場合で異なるので、順番に見ていきましょう。

ONNXRuntimeのヘッダー群とライブラリは以下のようなツリーになっているとします。

${ORT_ROOT}
├─include (ヘッダー群)
└─lib
    ├─Debug
    │  ├─shared (共有ライブラリ)
    │  │   ├─onnxruntime.dll
    │  │   └─onnxruntime.lib
    │  └─static (静的ライブラリ)
    │      ├─onnxruntime_common.lib
    │      ├─onnxruntime_flatbuffers.lib
    │      :
    │      └─external (依存ライブラリ)
    │          ├─clog.lib
    │          ├─cpuinfo.lib
    │          :
    ├─Release
    │  :
    :

共通部分

まずはおまじない。
MSVCの場合はCMAKE_MSVC_RUNTIME_LIBRARYの設定をONNXRuntimeのビルドの時と合わせるようにしましょう。

CMakeLists.txt
cmake_minimum_required(VERSION 3.15)
project(ORTResnet VERSION 0.1.0)

cmake_policy(SET CMP0048 NEW)

cmake_policy(SET CMP0091 NEW)
set(CMAKE_MSVC_RUNTIME_LIBRARY MultiThreaded$<$<CONFIG:Debug>:Debug>DLL)

set(CMAKE_CXX_STANDARD 17)

今回は普通にコンソールアプリケーションです。
ヘッダーまでは特に問題ありません。

CMakeLists.txt
# define target
add_executable(ORTResnet cpp/main.cpp)

if(MSVC)
    target_compile_options(ORTResnet PRIVATE /source-charset:utf-8)
endif()

target_include_directories(ORTResnet PRIVATE ${ORT_ROOT}/include)

動的リンク

ライブラリのリンク設定を書いていきます。動的リンクの場合は難しいことはないです。

CMakeLists.txt
target_link_directories(
    ORTResnet
PRIVATE
    ${ORT_ROOT}/lib/$<IF:$<CONFIG:Debug>,Debug,Release>/shared
)

target_link_libraries(
    ORTResnet
PRIVATE
    onnxruntime
)

静的リンク

問題は静的リンクです。
静的ライブラリは複数ある上に依存ライブラリもありますが、順番を間違えるとリンクエラーが(大量に)出てしまいます。
onnxruntime/cmake_guideline.md at master · microsoft/onnxruntime
onnxruntime/onnxruntime_dependencies.png at master · microsoft/onnxruntime

静的ライブラリのリンク順はONNXRuntimeビルド時のonnx_test_runnerプロジェクトを見るのが確実です。(MSVCの場合はonnx_test_runner.vcxprojbuild/Windows/[CONFIG]フォルダにあるはずです。)
How to build and use onnxruntime static lib on windows · Issue #1472 · microsoft/onnxruntime

WindowsかつDebug版の場合は、Windows組み込みのDbghelp.libが別途必要となります。
また、libprotobuf-liteのみ、Debug版で末尾にdがつくことに注意しましょう。

実際に書いてみると以下のようになります。

CMakeLists.txt
target_link_directories(
    ORTResnet
PRIVATE
    ${ORT_ROOT}/lib/$<IF:$<CONFIG:Debug>,Debug,Release>/static
    ${ORT_ROOT}/lib/$<IF:$<CONFIG:Debug>,Debug,Release>/static/external
)

target_link_libraries(
    ORTResnet
PRIVATE
    onnxruntime_session
    onnxruntime_optimizer
    onnxruntime_providers
    onnxruntime_util
    onnxruntime_framework
    onnxruntime_graph
    onnxruntime_mlas
    onnxruntime_common
    onnxruntime_flatbuffers
    onnx
    onnx_proto
    libprotobuf-lite$<$<CONFIG:Debug>:d>
    re2
    flatbuffers
    cpuinfo
    clog

    $<$<AND:$<PLATFORM_ID:Windows>,$<CONFIG:Debug>>:Dbghelp>
)

この順番でなくとも動く可能性はあります。またMSVC以外では確認していないので順番が異なる可能性があります。
リンクに失敗する場合は、ONNXRuntimeビルド時のonnx_test_runnerプロジェクトを確認してください。

以上でONNXRuntimeに関わる部分は終わりです。

全体

OpenCVも含めたCMakeLists.txt全体は以下のようになります。

CMakeLists.txt
cmake_minimum_required(VERSION 3.15)
project(ORTResnet VERSION 0.1.0)

cmake_policy(SET CMP0048 NEW)

cmake_policy(SET CMP0091 NEW)
set(CMAKE_MSVC_RUNTIME_LIBRARY MultiThreaded$<$<CONFIG:Debug>:Debug>DLL)

set(CMAKE_CXX_STANDARD 17)

option(ORT_STATIC "Use ONNXRuntime static library." ON)

# check ORT_ROOT existance
if (NOT ORT_ROOT)
    message(FATAL_ERROR "ORT_ROOT must be set")
endif()

get_filename_component(ORT_ROOT_ABS ${ORT_ROOT} ABSOLUTE)
if (EXISTS ${ORT_ROOT_ABS})
else()
    message(FATAL_ERROR "ORT_ROOT does not exists: ${ORT_ROOT}")
endif()

# define target
add_executable(ORTResnet cpp/main.cpp)

if(MSVC)
    target_compile_options(ORTResnet PRIVATE /source-charset:utf-8)
endif()

# ONNXRuntime
target_include_directories(ORTResnet PRIVATE ${ORT_ROOT}/include)

if(ORT_STATIC)
    target_link_directories(
        ORTResnet
    PRIVATE
        ${ORT_ROOT}/lib/$<IF:$<CONFIG:Debug>,Debug,Release>/static
        ${ORT_ROOT}/lib/$<IF:$<CONFIG:Debug>,Debug,Release>/static/external
    )

    target_link_libraries(
        ORTResnet
    PRIVATE
        onnxruntime_session
        onnxruntime_optimizer
        onnxruntime_providers
        onnxruntime_util
        onnxruntime_framework
        onnxruntime_graph
        onnxruntime_mlas
        onnxruntime_common
        onnxruntime_flatbuffers
        onnx
        onnx_proto
        libprotobuf-lite$<$<CONFIG:Debug>:d>
        re2
        flatbuffers
        cpuinfo
        clog

        $<$<AND:$<PLATFORM_ID:Windows>,$<CONFIG:Debug>>:Dbghelp>
    )

else()
    target_link_directories(
        ORTResnet
    PRIVATE
        ${ORT_ROOT}/lib/$<IF:$<CONFIG:Debug>,Debug,Release>/shared
    )

    target_link_libraries(
        ORTResnet
    PRIVATE
        onnxruntime
    )
endif()

# OpenCV
set(OpenCV_STATIC OFF)
find_package(OpenCV REQUIRED)

target_include_directories(ORTResnet PRIVATE ${OpenCV_INCLUDE_DIRS})
target_link_libraries(ORTResnet PRIVATE ${OpenCV_LIBS})

ビルドの実行

実際にビルドしてみましょう。
今回は、ORT_ROOTで前述のツリーがあるパス、ORT_STATICで静的リンクか動的リンクかを管理しています。

>cmake -B build -DORT_STATIC=ON -D ORT_ROOT=...
>cmake --build build --config Debug

ビルドに成功すればbuild/DebugフォルダにORTResnet.exeという名前の実行ファイルができているはずです。

推論アプリケーションの結果を確認

動的リンクの場合は、onnxruntime.dllがあるフォルダにパスを通すか、実行ファイルと同じフォルダにonnxruntime.dllをコピーして、実行時にロードできるようにしておきましょう。
また、opencv_world*.dllも同様です。

実際に実行してみます。

>ORTResnet.exe
1: Samoyed 0.873296
2: Pomeranian 0.0302708
3: white wolf 0.0196712
4: keeshond 0.0110735
5: Eskimo dog 0.00920427

pythonで確認した時と同じ結果が得られました。

おわりに

ONNXRuntimeをビルドしてから推論アプリケーションを作成するまでの流れを一通り追っていきました。
Execution Providersが絡むともう少し面倒なのですが、モチベがあれば後日別記事にしたいと思います。

この記事で紹介したコード等は以下のリポジトリから確認できます。(一部改変有)
mgmk2/onnxruntime-cpp-example

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
12