OpenCV

【C++】OpenCV dnnモジュールで実現する高速ディープラーニング推論:ONNXモデル読み込みからGPU活用まで

OpenCVのdnnモジュールをC++で活用すると、事前学習済みモデル(ONNXやTensorFlow)の読み込みから前処理、推論、後処理までを効率的に実装できます。

cv::dnn::blobFromImageで入力を整え、cv::Matでテンソルを扱い、CUDAバックエンドによるGPU推論で高速化も可能です。

ONNXモデルの準備と最適化

ディープラーニングモデルをC++とOpenCVのdnnモジュールで効率的に活用するためには、まずモデルのフォーマットやサイズを最適化することが重要です。

ここでは、ONNX(Open Neural Network Exchange)形式のモデルを準備し、最適化するための基本的な手順について詳しく解説します。

モデルフォーマットの確認方法

ONNXモデルを使用する前に、モデルファイルが正しいフォーマットであることを確認します。

ONNXモデルは通常.onnx拡張子のファイルとして保存されており、モデルの内容を確認するにはいくつかの方法があります。

まず、onnx Pythonパッケージを用いてモデルの構造を確認することが一般的です。

Python環境があれば、次のようなコードでモデルの情報を取得できます。

import onnx

# ONNXモデルのファイルパス

model_path = 'model.onnx'

# モデルの読み込み

model = onnx.load(model_path)

# モデルのグラフ情報を表示

print(onnx.helper.printable_graph(model.graph))

このコードはモデルの構造や入力・出力の情報を出力し、モデルが正しく読み込めるかどうかを確認できます。

また、コマンドラインツールのonnxruntimeを使ってモデルの推論を試すことも可能です。

これにより、モデルが正しく動作するかどうかを事前に検証できます。

外部ツールによる変換

多くのディープラーニングフレームワークは、学習済みモデルをONNX形式に変換することが可能です。

代表的なフレームワークごとの変換方法を紹介します。

TensorFlow→ONNX

TensorFlowモデルをONNXに変換するには、tf2onnxというツールを使用します。

まず、pipでインストールします。

pip install tf2onnx

次に、TensorFlowのSavedModelや.pbファイルからONNXモデルに変換します。

python -m tf2onnx.convert --saved-model tensorflow_model_directory --output model.onnx

または、.pbファイルから変換する場合は次のようにします。

python -m tf2onnx.convert --graphdef my_model.pb --output model.onnx

PyTorch→ONNX

PyTorchモデルは、torch.onnx.export関数を用いてONNXに変換します。

例として、学習済みのPyTorchモデルをONNXにエクスポートするコードを示します。

#include <torch/torch.h>
#include <torch/script.h>
#include <iostream>
int main() {
    // 事前に保存したPyTorchモデルの読み込み
    torch::jit::script::Module module;
    try {
        module = torch::jit::load("model.pt");
    }
    catch (const c10::Error& e) {
        std::cerr << "モデルの読み込みに失敗しました\n";
        return -1;
    }
    // ダミー入力の作成(バッチサイズ1、3チャンネル、224x224画像)
    std::vector<torch::jit::IValue> inputs;
    inputs.push_back(torch::rand({1, 3, 224, 224}));
    // ONNXにエクスポート
    try {
        torch::jit::ExportModuleOptions options;
        options.export_onnx = true;
        module.save("model.onnx");
        std::cout << "モデルをONNX形式で保存しました\n";
    }
    catch (const c10::Error& e) {
        std::cerr << "ONNXへのエクスポートに失敗しました\n";
        return -1;
    }
    return 0;
}

実際にはPyTorchのPython APIを使う方が一般的です。

C++からのエクスポートは制限があるため、通常はPython側で変換します。

モデルサイズ削減

モデルのサイズを削減することは、推論速度の向上やメモリ使用量の削減に直結します。

特に、GPUやエッジデバイスでの推論においては重要なポイントです。

以下に代表的な最適化手法を紹介します。

プルーニング

プルーニングは、モデルの不要なパラメータやニューロンを削除し、モデルの軽量化を図る手法です。

これにより、モデルのパラメータ数を減らし、推論速度を向上させることができます。

一般的なプルーニングの流れは次の通りです。

  1. 重要度の低いパラメータを特定
  2. それらをゼロに設定
  3. 再学習や微調整を行い、性能を維持

TensorFlowやPyTorchにはプルーニング用のライブラリやツールが用意されており、モデルの精度を保ちながら軽量化が可能です。

量子化

量子化は、モデルのパラメータや演算を低ビット幅の表現に変換することで、モデルのサイズと計算コストを削減します。

一般的には32ビット浮動小数点(FP32)から16ビット(FP16)やINT8に変換します。

量子化のメリットは次の通りです。

  • メモリ使用量の削減
  • 推論速度の向上
  • 一部のハードウェアでの高速化

ただし、量子化による精度低下のリスクもあるため、適用範囲や方法を選ぶ必要があります。

INT8量子化のメリット

INT8量子化は、モデルのパラメータと演算を8ビット整数に変換します。

これにより、次のようなメリットがあります。

  • メモリ消費の約4分の1に削減
  • ハードウェアアクセラレータの効率的な利用
  • 推論速度の大幅な向上

ただし、量子化誤差により精度が低下する可能性もあるため、キャリブレーションや微調整が必要です。

FP16量子化の適用

FP16(半精度浮動小数点)は、FP32と比べてビット幅が半分のため、モデルのサイズと計算コストを削減します。

多くのGPUはFP16演算に最適化されており、推論速度の向上が期待できます。

FP16量子化は、次のように適用します。

  • モデルのパラメータをFP16に変換
  • OpenCVのDNNバックエンド設定でFP16を有効化
  • 量子化誤差を抑えるためのキャリブレーションを行う

これらの最適化手法を適切に組み合わせることで、モデルの効率性と精度のバランスを取ることが可能です。

モデル読み込み

OpenCVのdnnモジュールを用いてディープラーニングモデルを読み込む際には、まずcv::dnn::Netオブジェクトを生成し、その後にモデルファイルを読み込みます。

これにより、画像認識や物体検出などの推論処理を行う準備が整います。

Netオブジェクトの生成

cv::dnn::Netは、OpenCVのdnnモジュールでモデルを管理し、推論を実行するための中心的なクラスです。

モデルの読み込みや推論の実行に必要な情報を保持し、操作を行います。

Netオブジェクトの生成は非常にシンプルです。

まず、空のNetオブジェクトを作成し、その後にモデルファイルを読み込みます。

#include <opencv2/dnn.hpp>
#include <opencv2/opencv.hpp>
#include <iostream>
int main() {
    // 空のNetオブジェクトを作成
    cv::dnn::Net net;
    // 以降にモデルの読み込み処理を記述
}

このnetオブジェクトに対して、モデルの読み込みや推論処理を行います。

ONNXモデルファイルの読み込み

ONNXモデルを読み込むには、cv::dnn::readNetFromONNX関数を使用します。

この関数は、ONNX形式のモデルファイルのパスを引数に取り、Netオブジェクトを返します。

#include <opencv2/dnn.hpp>
#include <opencv2/opencv.hpp>
#include <iostream>
int main() {
    // ONNXモデルのファイルパス
    std::string modelPath = "model.onnx";
    // ONNXモデルを読み込み、Netオブジェクトを作成
    cv::dnn::Net net = cv::dnn::readNetFromONNX(modelPath);
    if (net.empty()) {
        std::cerr << "モデルの読み込みに失敗しました。" << std::endl;
        return -1;
    }
    std::cout << "モデルの読み込みに成功しました。" << std::endl;
    // 以降に推論処理を記述
}

readNetFromONNXは、モデルの構造と重みを一度に読み込み、推論に必要な情報をNetオブジェクトに格納します。

これにより、モデルのロードが非常に簡単に行えます。

プロトファイルと重みファイルの利用

一部のモデルでは、構造を記述したプロトコルファイル(例:.prototxt.pbtxt)と重みファイル(例:.caffemodel.pb)を別々に管理しています。

OpenCVのdnnモジュールでは、これらのファイルを組み合わせてモデルを読み込むことも可能です。

例えば、Caffeモデルの場合は次のようにします。

#include <opencv2/dnn.hpp>
#include <opencv2/opencv.hpp>
#include <iostream>
int main() {
    // プロトファイルと重みファイルのパス
    std::string protoFile = "deploy.prototxt";
    std::string weightsFile = "weights.caffemodel";
    // モデルの読み込み
    cv::dnn::Net net = cv::dnn::readNetFromCaffe(protoFile, weightsFile);
    if (net.empty()) {
        std::cerr << "モデルの読み込みに失敗しました。" << std::endl;
        return -1;
    }
    std::cout << "モデルの読み込みに成功しました。" << std::endl;
    // 以降に推論処理を記述
}

また、TensorFlowの.pbファイルや、他のフレームワークのモデルも同様にreadNetFromTensorflowreadNet関数を使って読み込みます。

これらの方法を駆使して、必要なモデルファイルを適切に読み込み、推論処理の準備を整えます。

入力前処理

ディープラーニングモデルに画像を入力する前に、適切な前処理を行うことが重要です。

これにより、モデルの精度向上や推論速度の最適化が期待できます。

ここでは、画像の読み込みとリサイズ、カラースケール変換、そしてcv::dnn::blobFromImageを用いた正規化設定について詳しく解説します。

画像の読み込みとリサイズ処理

画像をモデルに入力するためには、まず画像ファイルを読み込み、そのサイズをモデルの期待する入力サイズにリサイズします。

OpenCVのcv::imread関数を使って画像を読み込み、cv::resize関数でリサイズを行います。

#include <opencv2/opencv.hpp>
#include <iostream>
int main() {
    // 画像ファイルのパス
    std::string imagePath = "input.jpg";
    // 画像の読み込み
    cv::Mat image = cv::imread(imagePath);
    if (image.empty()) {
        std::cerr << "画像の読み込みに失敗しました。" << std::endl;
        return -1;
    }
    // モデルの入力サイズ(例:224x224)
    int inputWidth = 224;
    int inputHeight = 224;
    // 画像のリサイズ
    cv::Mat resizedImage;
    cv::resize(image, resizedImage, cv::Size(inputWidth, inputHeight));
    // リサイズ後の画像を確認
    cv::imshow("Resized Image", resizedImage);
    cv::waitKey(0);
    return 0;
}

リサイズは、モデルの入力層の期待するサイズに合わせて行います。

これにより、モデルの入力フォーマットに適合させることができます。

カラースケール変換

多くのモデルはRGBカラー空間を前提としていますが、OpenCVはデフォルトでBGR形式で画像を読み込みます。

そのため、必要に応じてBGRからRGBへの変換を行います。

#include <opencv2/opencv.hpp>
#include <iostream>
int main() {
    // 画像の読み込み
    cv::Mat image = cv::imread("input.jpg");
    if (image.empty()) {
        std::cerr << "画像の読み込みに失敗しました。" << std::endl;
        return -1;
    }
    // BGRからRGBへの変換
    cv::Mat rgbImage;
    cv::cvtColor(image, rgbImage, cv::COLOR_BGR2RGB);
    // 変換後の画像を確認
    cv::imshow("RGB Image", rgbImage);
    cv::waitKey(0);
    return 0;
}

この変換は、モデルの学習時に使われたカラー空間と一致させるために重要です。

cv::dnn::blobFromImageによる正規化設定

cv::dnn::blobFromImageは、画像をモデルに入力可能な4次元のバッチ(NCHWまたはNHWC)に変換し、必要に応じて正規化やスケーリングを行います。

これにより、モデルの入力に適した形式に変換されます。

#include <opencv2/dnn.hpp>
#include <opencv2/opencv.hpp>
#include <iostream>
int main() {
    // 画像の読み込みとリサイズ
    cv::Mat image = cv::imread("input.jpg");
    if (image.empty()) {
        std::cerr << "画像の読み込みに失敗しました。" << std::endl;
        return -1;
    }
    int inputWidth = 224;
    int inputHeight = 224;
    cv::Mat resizedImage;
    cv::resize(image, resizedImage, cv::Size(inputWidth, inputHeight));
    // BGRからRGBに変換
    cv::cvtColor(resizedImage, resizedImage, cv::COLOR_BGR2RGB);
    // blobの作成と正規化設定
    // スケールファクタ、平均値、入力のサイズを指定
    double scaleFactor = 1.0 / 255.0; // ピクセル値を0-1に正規化
    cv::Scalar meanValues = cv::Scalar(0, 0, 0); // 平均値はモデルにより異なる
    bool swapRB = false; // 既にRGBに変換済みのためfalse
    // blobの作成
    cv::Mat inputBlob = cv::dnn::blobFromImage(resizedImage, scaleFactor, cv::Size(inputWidth, inputHeight), meanValues, swapRB, false);
    // 作成したblobの情報を出力
    std::cout << "Blobのサイズ: " << inputBlob.size << std::endl;
    return 0;
}

スケールファクタ

スケールファクタは、ピクセル値を正規化するために掛ける値です。

一般的には1/255を用いて、ピクセル値を0から1の範囲に変換します。

double scaleFactor = 1.0 / 255.0; // 0-255のピクセル値を0-1に正規化

平均値の設定

モデルによっては、入力画像から平均値を引く必要があります。

これは、学習時に使った平均画像の値に合わせるためです。

cv::Scalarで指定し、各チャネルごとに設定します。

cv::Scalar meanValues = cv::Scalar(123.68, 116.78, 103.94); // 例:ImageNet平均値

これらの設定を適切に行うことで、モデルの入力データが学習時と一致し、推論の精度向上につながります。

以上の前処理を行った後、inputBlobをモデルに入力し、推論を開始します。

GPU推論の高速化設定

GPUを用いた推論の高速化は、ディープラーニングアプリケーションのパフォーマンス向上において非常に重要です。

OpenCVのdnnモジュールでは、バックエンドとターゲットの設定を適切に行うことで、GPUの計算能力を最大限に引き出すことが可能です。

また、CUDAの設定を最適化することで、さらに推論速度を向上させることができます。

バックエンドとターゲットの指定

OpenCVのdnnモジュールでは、cv::dnn::Netの推論処理において、計算の実行環境を指定する必要があります。

これには、バックエンドとターゲットの設定が関わります。

DNN_BACKEND_CUDA

DNN_BACKEND_CUDAは、CUDAをバックエンドとして使用する設定です。

これにより、GPU上での高速な計算が可能となります。

#include <opencv2/dnn.hpp>
#include <opencv2/opencv.hpp>
#include <iostream>
int main() {
    cv::dnn::Net net = cv::dnn::readNetFromONNX("model.onnx");
    // CUDAバックエンドを設定
    net.setPreferableBackend(cv::dnn::DNN_BACKEND_CUDA);
    // CUDAをターゲットに設定
    net.setPreferableTarget(cv::dnn::DNN_TARGET_CUDA);
    // 以降に推論処理
}

この設定により、モデルの推論はGPU上で行われ、計算速度が大幅に向上します。

DNN_TARGET_CUDA_FP16

DNN_TARGET_CUDA_FP16は、FP16(半精度浮動小数点)を用いて推論を行う設定です。

これにより、メモリ使用量と計算コストを削減しつつ、推論速度をさらに向上させることが可能です。

#include <opencv2/dnn.hpp>
#include <opencv2/opencv.hpp>
#include <iostream>
int main() {
    cv::dnn::Net net = cv::dnn::readNetFromONNX("model.onnx");
    // CUDAバックエンドを設定
    net.setPreferableBackend(cv::dnn::DNN_BACKEND_CUDA);
    // FP16をターゲットに設定
    net.setPreferableTarget(cv::dnn::DNN_TARGET_CUDA_FP16);
    // 以降に推論処理
}

FP16を用いることで、特にGPUのTensorコアを活用した高速演算が可能となり、リアルタイム処理に適したパフォーマンスを実現します。

CUDA設定の最適化

CUDAを用いた推論の高速化には、ハードウェアの特性を最大限に活かすための設定や工夫が必要です。

バッファプリアロケーション

CUDAのバッファを事前に確保しておくことで、推論時のメモリ割り当てのオーバーヘッドを削減します。

OpenCVのdnnモジュールでは、setPreferableBackendsetPreferableTargetの設定と併せて、必要に応じてCUDAのメモリ管理を最適化します。

cv::cuda::setDevice(0); // 使用するGPUデバイスの選択
cv::cuda::Stream stream; // CUDAストリームの作成

ストリーム並列処理

複数のCUDAストリームを用いることで、推論とデータの前処理・後処理を並列に行い、全体の処理時間を短縮します。

OpenCVのcv::cuda::Streamを利用して、複数のストリームを管理します。

cv::cuda::Stream stream1, stream2;
// 複数のストリームを用いた並列処理

CUDAストリームの管理

CUDAストリームの適切な管理は、並列処理の効率化に直結します。

ストリーム間の依存関係を明確にし、必要に応じて同期を行うことで、データの競合や遅延を防ぎます。

stream1.waitForCompletion(); // ストリームの完了待ち
stream2.synchronize();       // 同期処理

これらの設定と工夫により、GPUの計算資源を最大限に活用し、推論の高速化を実現します。

特にリアルタイム性が求められるアプリケーションでは、これらの最適化が大きな効果をもたらします。

推論実行

推論を実行するには、まず準備したモデルに対して入力データを渡し、結果を得る必要があります。

OpenCVのdnnモジュールでは、Netクラスのforwardメソッドを呼び出すことで、モデルの推論を行います。

これにより、画像やデータの特徴抽出や分類結果を取得できます。

フォワードメソッドの呼び出し

forwardメソッドは、モデルの入力を受け取り、出力を返す最も基本的な推論関数です。

入力はcv::Matcv::cuda::GpuMatのblob形式で渡します。

#include <opencv2/dnn.hpp>
#include <opencv2/opencv.hpp>
#include <iostream>
int main() {
    // 既にモデルと入力blobの準備が完了していると仮定
    cv::dnn::Net net = cv::dnn::readNetFromONNX("model.onnx");
    // 画像の前処理とblob作成
    cv::Mat image = cv::imread("input.jpg");
    cv::Mat resizedImage;
    cv::resize(image, resizedImage, cv::Size(224, 224));
    cv::cvtColor(resizedImage, resizedImage, cv::COLOR_BGR2RGB);
    cv::Mat inputBlob = cv::dnn::blobFromImage(resizedImage, 1.0/255.0, cv::Size(224, 224), cv::Scalar(0,0,0), false, false);
    // 入力をセット
    net.setInput(inputBlob);
    // フォワード呼び出し
    cv::Mat output = net.forward();
    // 出力結果の確認
    std::cout << "推論結果の形状: " << output.size << std::endl;
    return 0;
}

この例では、setInputで入力データを設定し、その後forwardを呼び出すことで推論を実行しています。

中間レイヤー出力の取得

モデルの中間層の出力を取得したい場合は、forwardメソッドにレイヤー名やレイヤーインデックスを指定します。

これにより、特定の層の特徴マップや中間結果を抽出できます。

#include <opencv2/dnn.hpp>
#include <opencv2/opencv.hpp>
#include <iostream>
int main() {
    cv::dnn::Net net = cv::dnn::readNetFromONNX("model.onnx");
    // 画像の前処理とblob作成
    cv::Mat image = cv::imread("input.jpg");
    cv::Mat resizedImage;
    cv::resize(image, resizedImage, cv::Size(224, 224));
    cv::cvtColor(resizedImage, resizedImage, cv::COLOR_BGR2RGB);
    cv::Mat inputBlob = cv::dnn::blobFromImage(resizedImage, 1.0/255.0, cv::Size(224, 224), cv::Scalar(0,0,0), false, false);
    // 入力をセット
    net.setInput(inputBlob);
    // 中間層の出力を取得(例:層名が"conv1"の場合)
    std::vector<cv::String> layerNames = {"conv1"};
    std::vector<cv::Mat> intermediateOutputs;
    net.forward(intermediateOutputs, layerNames);
    // 中間層の出力を確認
    for (size_t i = 0; i < intermediateOutputs.size(); ++i) {
        std::cout << "層名: " << layerNames[i] << " の出力形状: " << intermediateOutputs[i].size << std::endl;
    }
    return 0;
}

forwardにレイヤー名のリストを渡すことで、その層の出力を複数取得できます。

推論結果の解釈

モデルの出力は、分類の場合はクラス確率やスコアのベクトル、検出モデルの場合はバウンディングボックスやクラスID、信頼度などの情報を含む構造体や行列となっています。

例として、画像分類の結果を解釈する場合は、出力の最大値とそのインデックスを取得します。

#include <opencv2/dnn.hpp>
#include <opencv2/opencv.hpp>
#include <iostream>
#include <algorithm>
int main() {
    cv::dnn::Net net = cv::dnn::readNetFromONNX("model.onnx");
    // 画像の前処理とblob作成
    cv::Mat image = cv::imread("input.jpg");
    cv::Mat resizedImage;
    cv::resize(image, resizedImage, cv::Size(224, 224));
    cv::cvtColor(resizedImage, resizedImage, cv::COLOR_BGR2RGB);
    cv::Mat inputBlob = cv::dnn::blobFromImage(resizedImage, 1.0/255.0, cv::Size(224, 224), cv::Scalar(0,0,0), false, false);
    // 入力をセット
    net.setInput(inputBlob);
    // 推論実行
    cv::Mat output = net.forward();
    // 出力の最大値とインデックスを取得
    cv::Point classIdPoint;
    double confidence;
    cv::minMaxLoc(output.reshape(1,1), 0, &confidence, 0, &classIdPoint);
    int classId = classIdPoint.x;
    std::cout << "予測クラスID: " << classId << ", 信頼度: " << confidence << std::endl;
    return 0;
}

この例では、出力を1次元に変形し、最大値の位置を特定して最も確信度の高いクラスを判定しています。

推論結果の解釈は、モデルの種類やタスクによって異なるため、出力の形式に応じて適切な処理を行います。

出力後処理

モデルの推論結果を実用的な情報に変換し、視覚的に理解しやすくするための後処理を行います。

特に、物体検出やセグメンテーションのタスクでは、複数の候補の中から最も適切なものを選び出し、画像に結果を描画する工程が必要です。

非最大抑制の適用

物体検出の結果には、多数の重複した候補バウンディングボックスが含まれることがあります。

これらを整理し、最も信頼性の高い候補だけを残すために非最大抑制(NMS:Non-Maximum Suppression)を適用します。

#include <opencv2/dnn.hpp>
#include <opencv2/opencv.hpp>
#include <vector>
#include <algorithm>
#include <iostream>
int main() {
    // 仮の検出結果(バウンディングボックス、信頼度、クラスID)
    std::vector<cv::Rect> boxes = {
        cv::Rect(50, 50, 100, 150),
        cv::Rect(60, 60, 100, 150),
        cv::Rect(200, 200, 80, 120)
    };
    std::vector<float> confidences = {0.9f, 0.85f, 0.75f};
    std::vector<int> classIds = {1, 1, 2};
    // NMSの閾値設定
    float nmsThreshold = 0.4f;
    // NMSの実行
    std::vector<int> indices;
    cv::dnn::NMSBoxes(boxes, confidences, 0.5f, nmsThreshold, indices);
    // 残った候補を表示
    for (int idx : indices) {
        std::cout << "選択されたボックス: " << boxes[idx] << " 信頼度: " << confidences[idx] << " クラスID: " << classIds[idx] << std::endl;
    }
    return 0;
}

この処理により、重複した検出候補を排除し、最も信頼性の高い結果だけを残すことができます。

バウンディングボックス描画

検出結果を画像に視覚的に示すために、バウンディングボックスとクラスラベルを描画します。

OpenCVのrectangleputText関数を用いて、結果を画像に重ねていきます。

#include <opencv2/opencv.hpp>
#include <vector>
#include <string>
int main() {
    cv::Mat image = cv::imread("input.jpg");
    if (image.empty()) {
        std::cerr << "画像の読み込みに失敗しました。" << std::endl;
        return -1;
    }
    // 仮の検出結果
    std::vector<cv::Rect> boxes = {cv::Rect(50, 50, 100, 150), cv::Rect(200, 200, 80, 120)};
    std::vector<std::string> classLabels = {"Cat", "Dog"};
    std::vector<float> confidences = {0.9f, 0.85f};
    // 色の設定(例:赤と青)
    cv::Scalar colors[] = {cv::Scalar(0, 0, 255), cv::Scalar(255, 0, 0)};
    for (size_t i = 0; i < boxes.size(); ++i) {
        // バウンディングボックスの描画
        cv::rectangle(image, boxes[i], colors[i], 2);
        // ラベルの作成
        std::string label = classLabels[i] + ": " + cv::format("%.2f", confidences[i]);
        // ラベルの位置
        int baseLine;
        cv::Size labelSize = cv::getTextSize(label, cv::FONT_HERSHEY_SIMPLEX, 0.5, 1, &baseLine);
        int top = std::max(boxes[i].y, labelSize.height);
        cv::rectangle(image, cv::Point(boxes[i].x, top - labelSize.height),
                      cv::Point(boxes[i].x + labelSize.width, top + baseLine),
                      colors[i], cv::FILLED);
        cv::putText(image, label, cv::Point(boxes[i].x, top),
                    cv::FONT_HERSHEY_SIMPLEX, 0.5, cv::Scalar(255,255,255), 1);
    }
    cv::imshow("Detection Results", image);
    cv::waitKey(0);
    return 0;
}

これにより、検出された物体の位置とクラス情報を画像上にわかりやすく表示できます。

マスク情報の合成処理

セグメンテーションモデルの出力には、各ピクセルのクラス確率やマスク情報が含まれます。

これらを画像に合成し、視覚的に理解しやすい形に変換します。

#include <opencv2/opencv.hpp>
#include <vector>
int main() {
    // 仮のマスク画像(例:1チャネルの確率マップ)
    cv::Mat maskProb = cv::Mat::zeros(224, 224, CV_32FC1);
    // 例として、中央に円形のマスクを作成
    cv::circle(maskProb, cv::Point(112, 112), 50, cv::Scalar(1.0), -1);
    // しきい値を設定して二値化
    cv::Mat mask;
    double threshold = 0.5;
    cv::threshold(maskProb, mask, threshold, 255, cv::THRESH_BINARY);
    // 3チャネルの画像に変換
    cv::Mat maskColor;
    cv::cvtColor(mask, maskColor, cv::COLOR_GRAY2BGR);
    // 元画像
    cv::Mat image = cv::imread("input.jpg");
    if (image.empty()) {
        std::cerr << "画像の読み込みに失敗しました。" << std::endl;
        return -1;
    }
    // マスクを適用して合成
    cv::Mat result;
    cv::addWeighted(image, 1.0, maskColor, 0.5, 0, result);
    cv::imshow("Segmentation Overlay", result);
    cv::waitKey(0);
    return 0;
}

この処理により、セグメンテーションの結果を画像に重ねて表示でき、対象物の領域を視覚的に把握しやすくなります。

これらの後処理を適切に行うことで、モデルの推論結果をわかりやすく、かつ実用的な形に変換できます。

パフォーマンス評価

モデルの推論速度やリソース消費を正確に把握し、最適化を図るためには、詳細なパフォーマンス評価が不可欠です。

ここでは、推論時間の計測方法、メモリ使用量の監視、そしてFPS(フレーム毎秒)の計算と最適化について詳しく解説します。

推論時間計測方法

推論時間の測定は、モデルの実用性やリアルタイム性を評価する上で重要です。

正確な計測には、処理の開始から終了までの時間を高精度で記録します。

高解像度画像での測定

高解像度画像を用いた推論時間の測定は、実際の運用環境に近い条件を再現するために有効です。

以下の例では、OpenCVのcv::TickMeterを用いて、画像の前処理から推論結果取得までの時間を計測します。

#include <opencv2/opencv.hpp>
#include <opencv2/dnn.hpp>
#include <iostream>
int main() {
    cv::TickMeter timer;
    cv::dnn::Net net = cv::dnn::readNetFromONNX("model.onnx");
    net.setPreferableBackend(cv::dnn::DNN_BACKEND_CUDA);
    net.setPreferableTarget(cv::dnn::DNN_TARGET_CUDA);
    cv::Mat image = cv::imread("high_res_input.jpg");
    cv::Mat resizedImage;
    cv::resize(image, resizedImage, cv::Size(1024, 1024)); // 高解像度画像
    cv::cvtColor(resizedImage, resizedImage, cv::COLOR_BGR2RGB);
    cv::Mat inputBlob = cv::dnn::blobFromImage(resizedImage, 1.0/255.0, cv::Size(1024, 1024), cv::Scalar(0,0,0), false, false);
    timer.start();
    net.setInput(inputBlob);
    cv::Mat output = net.forward();
    timer.stop();
    std::cout << "推論時間(高解像度画像): " << timer.getTimeMilli() << " ms" << std::endl;
    return 0;
}

この方法により、実際の画像サイズに基づいた推論時間を正確に測定できます。

バッチ処理時の比較

複数の画像を一度に処理するバッチ推論は、単一画像の推論と比較して効率性を評価するのに有効です。

バッチサイズを変えて推論時間を計測し、最適なバッチサイズを見つけることが重要です。

#include <opencv2/opencv.hpp>
#include <opencv2/dnn.hpp>
#include <iostream>
int main() {
    cv::dnn::Net net = cv::dnn::readNetFromONNX("model.onnx");
    net.setPreferableBackend(cv::dnn::DNN_BACKEND_CUDA);
    net.setPreferableTarget(cv::dnn::DNN_TARGET_CUDA);
    int batchSize = 4; // バッチサイズ例
    std::vector<cv::Mat> images(batchSize);
    for (int i = 0; i < batchSize; ++i) {
        cv::Mat img = cv::imread("input" + std::to_string(i) + ".jpg");
        cv::resize(img, img, cv::Size(224, 224));
        cv::cvtColor(img, img, cv::COLOR_BGR2RGB);
        images[i] = img;
    }
    cv::Mat inputBlob = cv::dnn::blobFromImages(images, 1.0/255.0, cv::Size(224, 224), cv::Scalar(0,0,0), false, false);
    cv::TickMeter timer;
    timer.start();
    net.setInput(inputBlob);
    cv::Mat outputs = net.forward();
    timer.stop();
    std::cout << "バッチ推論時間(バッチサイズ " << batchSize << "): " << timer.getTimeMilli() << " ms" << std::endl;
    return 0;
}

この結果をもとに、バッチサイズと推論速度の関係を分析し、最適なバッチ処理設定を決定します。

メモリ使用量の監視

推論中のメモリ消費量を監視することは、リソース制約のある環境や長時間運用において重要です。

CUDAのメモリ使用量は、cudaMemGetInfo関数やNVIDIAのnvidia-smiコマンドを用いて確認できます。

nvidia-smi --query-gpu=memory.used,memory.free --format=csv

また、プログラム内でCUDA APIを呼び出して動的に監視することも可能です。

#include <cuda_runtime.h>
#include <iostream>
int main() {
    size_t free_mem, total_mem;
    cudaMemGetInfo(&free_mem, &total_mem);
    std::cout << "空きメモリ: " << free_mem / (1024 * 1024) << " MB" << std::endl;
    std::cout << "総メモリ: " << total_mem / (1024 * 1024) << " MB" << std::endl;
    return 0;
}

これにより、推論中のメモリ使用状況をリアルタイムで把握できます。

FPS計算と最適化

リアルタイムアプリケーションでは、FPS(Frames Per Second)を計測し、パフォーマンスの指標とします。

FPSは、一定時間内に処理したフレーム数を示し、次の式で計算します。

FPS=1平均推論時間(秒)

実装例は以下の通りです。

#include <opencv2/opencv.hpp>
#include <opencv2/dnn.hpp>
#include <vector>
#include <iostream>
int main() {
    cv::dnn::Net net = cv::dnn::readNetFromONNX("model.onnx");
    net.setPreferableBackend(cv::dnn::DNN_BACKEND_CUDA);
    net.setPreferableTarget(cv::dnn::DNN_TARGET_CUDA);
    int frameCount = 0;
    double totalTime = 0.0;
    cv::TickMeter timer;
    while (true) {
        cv::Mat frame = /* 取得する画像フレーム */;
        timer.start();
        // 前処理と推論
        cv::Mat inputBlob = cv::dnn::blobFromImage(frame, 1.0/255.0, cv::Size(224, 224));
        net.setInput(inputBlob);
        cv::Mat output = net.forward();
        timer.stop();
        totalTime += timer.getTimeMilli() / 1000.0; // 秒単位
        frameCount++;
        double fps = frameCount / totalTime;
        std::cout << "現在のFPS: " << fps << std::endl;
        timer.reset();
        // ループ終了条件
    }
    return 0;
}

最適化には、モデルの軽量化、バッチ処理の導入、GPUの効率的な利用、非同期処理の活用などが考えられます。

これらを組み合わせて、リアルタイム性と精度のバランスを追求します。

エラー解析

ディープラーニング推論の実行中に発生するエラーの原因を特定し、適切に対処することは、システムの安定性と信頼性を維持するために不可欠です。

ここでは、モデルの読み込み失敗時の検証方法、前処理フォーマットの不一致を解消する手段、そしてCUDAランタイムエラーへの対策について詳しく解説します。

モデル読み込み失敗時の検証

モデルの読み込みに失敗した場合、多くはファイルのパスやフォーマットの誤り、モデルファイルの破損が原因です。

まず、以下のポイントを確認します。

  1. ファイルパスの正確性

指定したモデルファイルのパスが正しいか、存在しているかを確認します。

絶対パスや相対パスの誤りも原因となるため、絶対パスを用いると確実です。

  1. ファイルの整合性

ファイルが破損していないか、正しいフォーマットで保存されているかを検証します。

fileコマンドやfileツールを使って、ファイルタイプを確認します。

  1. モデルの互換性

使用しているOpenCVやdnnモジュールのバージョンとモデルのフォーマットが互換性があるかを確認します。

特に、ONNXモデルはバージョンによってサポート範囲が異なるため、推奨バージョンを使用します。

  1. 例外処理の追加

readNetFromONNXや他の読み込み関数は例外を投げることがあるため、try-catchブロックを用いてエラーを捕捉し、詳細なエラーメッセージを出力します。

try {
    cv::dnn::Net net = cv::dnn::readNetFromONNX("model.onnx");
} catch (const cv::Exception& e) {
    std::cerr << "モデルの読み込みに失敗しました: " << e.what() << std::endl;
}

これにより、エラーの原因を特定しやすくなります。

前処理フォーマット不一致の解消

入力データのフォーマットがモデルの期待と一致しない場合、推論結果が不正確になるだけでなく、エラーの原因にもなります。

主な原因と対策は以下の通りです。

  1. カラー空間の不一致

モデルがRGBを前提としているのに対し、OpenCVはデフォルトでBGRで画像を読み込むため、cv::cvtColorを用いて変換します。

  1. 入力サイズの不一致

モデルの入力サイズ(例:224×224)にリサイズしているかを確認します。

cv::resizeを用いて正確に調整します。

  1. データ型の不一致

blobFromImageのスケールや平均値設定が正しいか、また、必要に応じてデータ型をCV_32Fに変換します。

  1. 正規化パラメータの誤り

スケールファクタや平均値の設定ミスは、推論結果に大きな影響を与えるため、モデルの学習時の設定と一致させる必要があります。

cv::Mat inputBlob = cv::dnn::blobFromImage(resizedImage, 1.0/255.0, cv::Size(224, 224), cv::Scalar(123.68, 116.78, 103.94), true, false);

この例では、平均値を引き、RGBに変換しています。

CUDAランタイムエラー対策

CUDAを用いた推論中に発生するエラーは、ドライバやライブラリの不整合、リソース不足、またはプログラムの誤用に起因します。

対策は以下の通りです。

  1. CUDAドライバとライブラリのバージョン確認

nvidia-smiコマンドやnvcc --versionで、ドライバとCUDAツールキットのバージョンを確認し、推奨バージョンを使用します。

  1. GPUメモリの空き容量を確保

nvidia-smicudaMemGetInfoを用いて、メモリの空き容量を監視し、不要なプロセスを停止します。

  1. メモリリークの防止

CUDAのリソースを適切に解放し、不要なメモリ割り当てを避けます。

cudaFreecv::cuda::Stream::waitForCompletion()を活用します。

  1. エラーコードの取得と対処

CUDA API呼び出しの返り値を確認し、エラーコードに応じた処理を行います。

cudaError_t err = cudaSetDevice(0);
if (err != cudaSuccess) {
    std::cerr << "CUDAデバイス設定エラー: " << cudaGetErrorString(err) << std::endl;
}
  1. デバッグとログ出力

CUDAのエラーは詳細なログ出力を有効にし、原因を特定します。

export CUDA_LAUNCH_BLOCKING=1

これにより、エラーの発生箇所を特定しやすくなります。

これらの対策を講じることで、CUDAランタイムエラーの発生頻度を低減し、安定した推論環境を構築できます。

実践的活用例

ディープラーニングモデルとOpenCVのdnnモジュールを活用した実践的なアプリケーション例を紹介します。

これらの例は、実際のシステムやサービスに組み込む際の参考となるもので、画像分類、オブジェクト検出、セグメンテーションの3つの代表的なタスクについて詳しく解説します。

画像分類アプリケーション

画像分類は、入力された画像を特定のクラスに分類するタスクです。

例えば、猫や犬、車、風景などのカテゴリに画像を振り分けるアプリケーションに適しています。

#include <opencv2/dnn.hpp>
#include <opencv2/opencv.hpp>
#include <iostream>
#include <vector>
#include <string>
int main() {
    // モデルの読み込み
    cv::dnn::Net net = cv::dnn::readNetFromONNX("classification_model.onnx");
    net.setPreferableBackend(cv::dnn::DNN_BACKEND_CUDA);
    net.setPreferableTarget(cv::dnn::DNN_TARGET_CUDA);
    // 画像の読み込みと前処理
    cv::Mat image = cv::imread("test_image.jpg");
    cv::Mat resizedImage;
    cv::resize(image, resizedImage, cv::Size(224, 224));
    cv::cvtColor(resizedImage, resizedImage, cv::COLOR_BGR2RGB);
    cv::Mat inputBlob = cv::dnn::blobFromImage(resizedImage, 1.0/255.0, cv::Size(224, 224), cv::Scalar(0,0,0), true, false);
    // 推論
    net.setInput(inputBlob);
    cv::Mat output = net.forward();
    // 結果の解釈
    cv::Point classIdPoint;
    double confidence;
    cv::minMaxLoc(output.reshape(1,1), 0, &confidence, 0, &classIdPoint);
    int classId = classIdPoint.x;
    // クラス名のリスト(例)
    std::vector<std::string> classNames = {"猫", "犬", "車", "風景"};
    std::cout << "予測クラス: " << classNames[classId] << ", 信頼度: " << confidence << std::endl;
    return 0;
}

このアプリケーションは、画像を入力し、モデルの出力から最も確信度の高いクラスを判定します。

リアルタイムの画像認識や画像整理システムに応用可能です。

リアルタイムオブジェクト検出

動画やカメラ映像からリアルタイムで物体を検出し、バウンディングボックスとクラスラベルを表示します。

YOLOやSSDなどの検出モデルを用いることが多いです。

#include <opencv2/dnn.hpp>
#include <opencv2/opencv.hpp>
#include <iostream>
#include <vector>
int main() {
    // モデルの読み込み
    cv::dnn::Net net = cv::dnn::readNetFromDarknet("yolov3.cfg", "yolov3.weights");
    net.setPreferableBackend(cv::dnn::DNN_BACKEND_CUDA);
    net.setPreferableTarget(cv::dnn::DNN_TARGET_CUDA);
    // カメラキャプチャ
    cv::VideoCapture cap(0);
    if (!cap.isOpened()) {
        std::cerr << "カメラを開けません。" << std::endl;
        return -1;
    }
    std::vector<std::string> classNames = {/* クラス名リスト */};
    while (true) {
        cv::Mat frame;
        cap >> frame;
        if (frame.empty()) break;
        // 前処理
        cv::Mat blob = cv::dnn::blobFromImage(frame, 1/255.0, cv::Size(416, 416), cv::Scalar(), true, false);
        net.setInput(blob);
        // 推論
        std::vector<cv::Mat> outputs;
        net.forward(outputs, net.getUnconnectedOutLayersNames());
        // 検出結果の後処理(閾値やNMS)
        // 省略:詳細は前述の非最大抑制と描画の例を参照
        // 検出結果を描画
        // 省略:バウンディングボックスとラベルの描画
        cv::imshow("Object Detection", frame);
        if (cv::waitKey(1) == 27) break; // ESCキーで終了
    }
    return 0;
}

この例は、リアルタイムの映像から複数の物体を検出し、即座に結果を表示します。

監視システムや自動運転支援などに応用できます。

セマンティックセグメンテーションパイプライン

画像や映像の各ピクセルに対してクラスラベルを割り当てるセグメンテーションは、医療画像解析や自動運転の環境理解に不可欠です。

#include <opencv2/dnn.hpp>
#include <opencv2/opencv.hpp>
#include <iostream>
int main() {
    // セグメンテーションモデルの読み込み
    cv::dnn::Net net = cv::dnn::readNetFromONNX("segmentation_model.onnx");
    net.setPreferableBackend(cv::dnn::DNN_BACKEND_CUDA);
    net.setPreferableTarget(cv::dnn::DNN_TARGET_CUDA);
    // 入力画像の読み込み
    cv::Mat image = cv::imread("road_scene.jpg");
    cv::Mat resizedImage;
    cv::resize(image, resizedImage, cv::Size(512, 512));
    cv::cvtColor(resizedImage, resizedImage, cv::COLOR_BGR2RGB);
    cv::Mat inputBlob = cv::dnn::blobFromImage(resizedImage, 1.0/255.0, cv::Size(512, 512), cv::Scalar(), true, false);
    // 推論
    net.setInput(inputBlob);
    cv::Mat output = net.forward();
    // 出力の後処理
    // 例:ピクセルごとのクラスラベルを取得し、色付けして表示
    cv::Mat segmentationMap(output.size[2], output.size[3], CV_8UC1);
    for (int y = 0; y < output.size[2]; ++y) {
        for (int x = 0; x < output.size[3]; ++x) {
            float maxScore = -FLT_MAX;
            int maxClass = 0;
            for (int c = 0; c < output.size[1]; ++c) {
                float score = output.at<float>(0, c, y, x);
                if (score > maxScore) {
                    maxScore = score;
                    maxClass = c;
                }
            }
            segmentationMap.at<uchar>(y, x) = static_cast<uchar>(maxClass);
        }
    }
    // 色付けと表示
    cv::Mat colorMap;
    // 例:クラスごとに色付け
    // 省略:色付け処理
    cv::imshow("Segmentation Result", colorMap);
    cv::waitKey(0);
    return 0;
}

このパイプラインは、道路シーンの理解や医療画像の解析など、多様な分野での応用が可能です。

モデルの出力を適切に解釈し、視覚化することで、システムの理解と改善に役立ちます。

まとめ

この記事では、OpenCVのdnnモジュールを用いたディープラーニングモデルの活用方法を解説しました。

モデルの準備や入力前処理、GPU高速化設定、推論の実行と結果の後処理、パフォーマンス評価、エラー対策、そして実践的なアプリケーション例まで幅広く紹介しています。

これらの知識を活用すれば、画像分類や物体検出、セグメンテーションなどのシステムを効率的に構築できるようになります。

関連記事

Back to top button
目次へ