YOLOv9を用いた物体検出AIをC++とQtで作ったGUIアプリに組み込む

# C++でYOLOv9を動かします

流れとしては以下のようになります。

- PythonでYOLOv9の動作を確認します
- YOLOv9のモデルをONNX形式でエクスポートします
- C++用のONNXRUNTIMEを使用してモデルを読み込みます
- QtでGUIアプリを作ります

# 筆者の開発環境

- Arch Linux
- Python 3.12

# PythonでYOLOv9の動作を確認する

### Pythonの仮想環境を作成する
``` .sh
python -m venv yolov9env
```
### Pythonの仮想環境を有効化する
``` .sh
cd yolov9env
source bin/activate
```

### YOLOv9を準備する
``` .sh
git clone https://github.com/WongKinYiu/yolov9.git
cd yolov9
pip install -r requirements.txt
```

### 事前学習済みデータを取得する
``` .sh
wget -P weights -q https://github.com/WongKinYiu/yolov9/releases/download/v0.1/yolov9-c.pt
```

### 推論を実行する
``` .sh
python detect.py --weights weights/yolov9-c.pt --conf 0.1 --source data/images/horses.jpg --device 0 --exist-ok
```

最初は `AttributeError: 'list' object has no attribute 'device'` というエラーが出ると思います。その場合は、 https://github.com/WongKinYiu/yolov9/issues/11 の記述に従って `utils/general.py` の903行を以下のように修正します

``` utils/general.py
        prediction = prediction[0][0]  # select only inference output
```

修正したら、もう一度推論を実行します。

正常に実行できたら、結果の画像が `runs/detect/exp` に保存されます。

```
(yolov9env) soramimi@alice:~/py/yolov9env/yolov9$ ls -ls runs/detect/exp
合計 156
156 -rw-r--r-- 1 soramimi soramimi 157641  5月  4 23:15 horses.jpg
```
できた画像ファイルを見てみましょう。
![horses.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/61222/5efbd1df-c8db-f99e-88db-c033694e00e9.jpeg)

# YOLOv9のモデルをONNX形式でエクスポート

### onnxをインストールします

``` .sh
pip install onnx
```

### 推論のためのPythonスクリプトを改造します

``` .sh
cp detect.py saveonnx.py
```

インポートを追加します

``` saveonnx.py
import torch.onnx
```
`    # Dataloader`の行から下、`def parse_opt():`の行の手前までを削除します。

削除した行の代わりに、以下の行を追加します。

``` saveonnx.py
   torch.onnx.export(model, torch.randn(1, 3, *imgsz).to(device), 'yolov9.onnx')
```

### ONNX形式でエクスポートします

``` .sh
python saveonnx.py --weights weights/yolov9-c.pt --device 0
```

`yolov9.onnx`ができたら、[NETRON](https://netron.app/)で可視化してみましょう。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/61222/788bffbf-6111-e27e-2a5f-dbaa8042f114.png)

### ONNX形式を確認

いちばん上の`input0`をクリックすると、右側にモデルの情報が表示されます。入力は`[1, 3, 640, 640]`のテンソルです。これは、1枚の3チャンネル(RGB)高さ640幅640のデータを入力することを意味します。出力は8つありますが、`output0`だけ使います。これは`[1, 84, 8400]`のテンソルを出力します。84要素のデータが8400個あることを示しています（注意点があります：後述）。84要素のデータの中には、検出されたバウンディングボックスの座標（4要素）と、クラス分類数（80）が含まれています。

# C++でONNXRUNTIMEを利用するコードを作る

参考

https://qiita.com/mgmk2/items/c802598b9dbe7a06a425

### 作業ディレクトリを作る
``` .sh
cd ..
mkdir work
cd work
```

### C++のコードを実装します

冒頭部分では、インクルードの指定、結果を格納する構造体の定義、ONNXRUNTIMEで使用する変数などの定義、初期化を行います。

``` main.cpp
#include <onnxruntime/onnxruntime_cxx_api.h>
#include <memory.h>
#include <iostream>

struct BoundingBox {
	int index;
	float score;
	float x, y, w, h;
};

int main()
{
	std::unique_ptr<Ort::Env> env;
	std::unique_ptr<Ort::MemoryInfo> memory_info;
	
	Ort::SessionOptions session_options;
	std::unique_ptr<Ort::Session> session;
	
	size_t num_input_nodes = 0;
	size_t num_output_nodes = 0;
	std::vector<char const *> input_node_names;
	std::vector<char const *> output_node_names;
	std::vector<std::string> input_node_name_strings;
	std::vector<std::string> output_node_name_strings;
	
	// ONNX Runtime環境の初期化
	env = std::make_unique<Ort::Env>(ORT_LOGGING_LEVEL_WARNING, "test");
	
	memory_info = std::make_unique<Ort::MemoryInfo>(Ort::MemoryInfo::CreateCpu(OrtDeviceAllocator, OrtMemTypeCPU));
	
	// 推論セッションの設定
	session_options.SetIntraOpNumThreads(1);
```

続いて、モデルの読み込みを実装します。ONNXRUNTIMEセッションの作成と、入出力情報を取得しています。

``` main.cpp
	char const *model_path = "../yolov9/yolov9.onnx";

	// モデルの読み込み
	try {
		// セッションの生成
		session = std::make_unique<Ort::Session>(*env, model_path, session_options);
		
		// 入出力情報の取得
		num_input_nodes = session->GetInputCount();
		num_output_nodes = session->GetOutputCount();
		// qDebug() << "Number of input nodes:" << num_input_nodes;
		// qDebug() << "Number of output nodes:" << num_output_nodes;
		
		Ort::AllocatorWithDefaultOptions allocator;
		
 		// 入力ノード名の取得		
		input_node_names.resize(num_input_nodes);
		input_node_name_strings.resize(num_input_nodes);
		for (size_t i = 0; i < num_input_nodes; i++) {
			input_node_name_strings[i] = session->GetInputNameAllocated(i, allocator).get();
			input_node_names[i] = input_node_name_strings[i].c_str();
			// qDebug() << "input:" << input_node_names[i];
		}
		
		// 出力ノード名の取得
		output_node_names.resize(num_output_nodes);
		output_node_name_strings.resize(num_output_nodes);
		for (size_t i = 0; i < num_output_nodes; i++) {
			output_node_name_strings[i] = session->GetOutputNameAllocated(i, allocator).get();
			output_node_names[i] = output_node_name_strings[i].c_str();
			// qDebug() << "output:" << output_node_names[i];
		}
		
	} catch (const Ort::Exception &e) {
		std::cerr << "Error:" << e.what() << std::endl;
		exit(1);
	}
```

推論処理の実装です。

``` main.cpp
	const int N = 1; // batch size
	const int C = 3; // number of channels
	const int W = 640; // width
	const int H = 640; // height
	
	// 入力データの準備
	std::vector<float> input_tensor_values(N * C * H * W);

	// ★★★ TODO: ここで画像を読み込んでinput_tensor_valuesに格納する ★★★
	
	// 入力テンソルの生成
	std::vector<int64_t> input_tensor_shape = {N, C, H, W};
	Ort::Value input_tensor = Ort::Value::CreateTensor<float>(*memory_info, input_tensor_values.data(), input_tensor_values.size(), input_tensor_shape.data(), input_tensor_shape.size());
	
	// 推論の実行
	auto output_tensors = session->Run(Ort::RunOptions{nullptr}, input_node_names.data(), &input_tensor, 1, output_node_names.data(), num_output_nodes);
```

出力テンソルを解読して、バウンディングボックスと推論スコアを取得します。

COCOデータセットによる事前学習データの場合、出力テンソルは [1, 84, 8400] となります。これは、84個の`float`値が8400個あるということです。概念的には以下のような構造体が8400個あるのですが、ここで注意が必要です。

``` .cpp
struct Item {
    float x;
    float y;
    float w;
    float h;
    float score[80];
};
```

テンソルの形式が [1, 8400, 84] であるならば話は簡単です。しかし実際には [1, 84, 8400] となっています。つまり、8400個毎の間隔で84回値を拾うことで、上記のような構造体が8400個得られます。転置するのも面倒ですし無駄なので、次のようなラムダ式を利用することにします。

``` .cpp
	const int count = shape[2];

    auto Value = [&](int index){
		return output_tensor[count * index + i];
	};
```

これを使用すると、次のように値を取得できます。

- `Value(0)` : X座標
- `Value(1)` : Y座標
- `Value(2)` : 幅
- `Value(3)` : 高さ
- `Value(4)`〜`Value(83)` : 80分類分の各スコア値

座標データは正規化されておらず、[0, 640]の範囲となっています。スコアは[0, 1]の範囲です。

以上を踏まえて、出力テンソルからバウンディングボックスの情報を読み取る処理を実装します。

``` main.cpp
	// 結果を生成
	std::vector<BoundingBox> bboxes;
	
	auto info = output_tensors.front().GetTensorTypeAndShapeInfo();
	std::vector<int64_t> shape = info.GetShape(); // e.g. {1, 84, 8400}
	
	const int values = shape[1]; // maybe 84 if coco dataset
	const int classes = values - 4; // 80 classes
	const int count = shape[2];
	
	float const *output_tensor = output_tensors[0].GetTensorData<float>();
	
	for (int i = 0; i < count; i++) {
		auto Value = [&](int index){
			return output_tensor[count * index + i];
		};
		// x, y, w, h, class0, class1, ...
		float x = Value(0);
		float y = Value(1);
		float w = Value(2);
		float h = Value(3);
		x -= w / 2;
		y -= h / 2;
		for (int j = 0; j < classes; j++) { // クラス数
			BoundingBox bbox;
			bbox.score = Value(4 + j);
			if (bbox.score > 0.5) { // 一定よりスコアが高ければ
				bbox.index = j;
				bbox.x = x;
				bbox.y = y;
				bbox.w = w;
				bbox.h = h;
				bboxes.push_back(bbox); // 記録する
			}
		}
	}
```

以上で、推論処理の主要な部分の実装はできました。

画像を読み込んで入力テンソルを作る処理と、推論結果のバウンディングボックスを描画する処理は、QtのGUIアプリで実装します。

<details><summary>ソースコード全体</summary>

``` main.cpp
#include <onnxruntime/onnxruntime_cxx_api.h>
#include <memory.h>
#include <iostream>

struct BoundingBox {
	int index;
	float score;
	float x, y, w, h;
};

int main()
{
	std::unique_ptr<Ort::Env> env;
	std::unique_ptr<Ort::MemoryInfo> memory_info;
	
	Ort::SessionOptions session_options;
	std::unique_ptr<Ort::Session> session;
	
	size_t num_input_nodes = 0;
	size_t num_output_nodes = 0;
	std::vector<char const *> input_node_names;
	std::vector<char const *> output_node_names;
	std::vector<std::string> input_node_name_strings;
	std::vector<std::string> output_node_name_strings;
	
	char const *model_path = "../yolov9/yolov9.onnx";

	// ONNX Runtime環境の初期化
	env = std::make_unique<Ort::Env>(ORT_LOGGING_LEVEL_WARNING, "test");
	
	memory_info = std::make_unique<Ort::MemoryInfo>(Ort::MemoryInfo::CreateCpu(OrtDeviceAllocator, OrtMemTypeCPU));
	
	// 推論セッションの設定
	session_options.SetIntraOpNumThreads(1);

	// モデルの読み込み
	try {
		// セッションの生成
		session = std::make_unique<Ort::Session>(*env, model_path, session_options);
		
		// 入出力情報の取得
		num_input_nodes = session->GetInputCount();
		num_output_nodes = session->GetOutputCount();
		// qDebug() << "Number of input nodes:" << num_input_nodes;
		// qDebug() << "Number of output nodes:" << num_output_nodes;
		
		Ort::AllocatorWithDefaultOptions allocator;
		
 		// 入力ノード名の取得		
		input_node_names.resize(num_input_nodes);
		input_node_name_strings.resize(num_input_nodes);
		for (size_t i = 0; i < num_input_nodes; i++) {
			input_node_name_strings[i] = session->GetInputNameAllocated(i, allocator).get();
			input_node_names[i] = input_node_name_strings[i].c_str();
			// qDebug() << "input:" << input_node_names[i];
		}
		
		// 出力ノード名の取得
		output_node_names.resize(num_output_nodes);
		output_node_name_strings.resize(num_output_nodes);
		for (size_t i = 0; i < num_output_nodes; i++) {
			output_node_name_strings[i] = session->GetOutputNameAllocated(i, allocator).get();
			output_node_names[i] = output_node_name_strings[i].c_str();
			// qDebug() << "output:" << output_node_names[i];
		}
		
	} catch (const Ort::Exception &e) {
		std::cerr << "Error:" << e.what() << std::endl;
		exit(1);
	}

	const int N = 1; // batch size
	const int C = 3; // number of channels
	const int W = 640; // width
	const int H = 640; // height
	
	// 入力データの準備
	std::vector<float> input_tensor_values(N * C * H * W);

	// TODO: ここで画像を読み込んでinput_tensor_valuesに格納する
	
	// 入力テンソルの生成
	std::vector<int64_t> input_tensor_shape = {1, 3, H, W};
	Ort::Value input_tensor = Ort::Value::CreateTensor<float>(*memory_info, input_tensor_values.data(), input_tensor_values.size(), input_tensor_shape.data(), input_tensor_shape.size());
	
	// 推論の実行
	auto output_tensors = session->Run(Ort::RunOptions{nullptr}, input_node_names.data(), &input_tensor, 1, output_node_names.data(), num_output_nodes);
	
	// 結果を生成
	std::vector<BoundingBox> bboxes;
	
	auto info = output_tensors.front().GetTensorTypeAndShapeInfo();
	std::vector<int64_t> shape = info.GetShape(); // e.g. {1, 84, 8400}
	
	const int values = shape[1]; // maybe 84 if coco dataset
	const int classes = values - 4; // 80 classes
	const int count = shape[2];
	
	float const *output_tensor = output_tensors[0].GetTensorData<float>();
	
	for (int i = 0; i < count; i++) {
		auto Value = [&](int index){
			return output_tensor[count * index + i]; // 最も内側の次元がグリッドセル数（8400）であることに注意
		};
		// x, y, w, h, class0, class1, ...
		float x = Value(0);
		float y = Value(1);
		float w = Value(2);
		float h = Value(3);
		x -= w / 2;
		y -= h / 2;
		for (int j = 0; j < classes; j++) { // クラス数
			BoundingBox bbox;
			bbox.score = Value(4 + j);
			if (bbox.score > 0.5) { // 一定よりスコアが高ければ
				bbox.index = j;
				bbox.x = x;
				bbox.y = y;
				bbox.w = w;
				bbox.h = h;
				bboxes.push_back(bbox); // 記録する
			}
		}
	}
	
	// 結果の表示
	for (auto &item : bboxes) {
		std::cerr << "class:" << item.index << "score:" << item.score << "x:" << item.x << "y:" << item.y << "w:" << item.w << "h:" << item.h << std::endl;
	}

	return 0;
}

```

</details>


# QtでGUIアプリを作る

以下、QtでGUIアプリを開発するチュートリアル寄りの記事となっています。

Qt CreatorでGUIアプリケーションを開発する手順を説明しますが、最終的に物体検出AIを組み込むと、ソースコードがかなり大きくなりますので、プログラム全体は[筆者のGitHubリポジトリ](https://github.com/soramimi/QtYOLOv9)から取得してください。

### 環境構築

`pacman`（Arch Linux など）や`apt`（Ubuntu など）で、`qtcreator`とQt5またはQt6の開発パッケージをインストールします。パッケージ名はディストリビューションによって異なりますが、`qt5-base`とか、`qtbase5-dev`のようなパッケージです。最近のディストリビューションではQt5だけでなくQt6のパッケージが用意されているかもしれません。もちろん新しい方をおすすめします。

Qt関連のパッケージの他には、`onnxruntime`が必要ですので、インストールしておきます。

### プロジェクトを作成

Qt Creator を起動します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/61222/bf341109-7222-0732-660a-7e14991811a3.png)

Qt Creator の左上の Create Project... という緑色のボタンがありますので、これをクリックします。


![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/61222/fbf00be5-9b8e-27b1-3900-79cc0a74de62.png)

Qtウィジェットアプリケーションを選択します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/61222/719d0901-5abb-c73b-b33e-98e504135153.png)

「QtYOLOv9」という名前で新規プロジェクトを作成します。

ビルドシステムは「qmake」と「CMake」が選べます。どちらでもいいと思いますが、筆者は qmake を使用しています。

クラス情報の画面で、`MainWindow`の名前を指定することができます。変更しないでそのまま進みます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/61222/d8994ed0-831f-ce9d-eac2-62325553616c.png)

キットの選択画面では、インストールされているQtのバージョンを選ぶことができます。普通は「デスクトップ」で構いません。「Qt5」だったり「Qt6」だったりすることがあるかもしれません。そういったものが一つも選べない場合は、Qtのインストールが不完全ですので、前述のパッケージが正しくインストールされているか確認します。「Debug」と「Release」を有効にします。「Profile」はあってもなくても構いませんが、多分使いません。

完了ボタンを押すと、アプリケーションの雛形ができます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/61222/d4a9e277-4374-6014-3fea-48e6a04a0c8a.png)

### 画像表示クラスを作る

プロジェクトツリーで右クリックして、「新しいファイルを追加...」を選択します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/61222/6f12691f-a9ac-2f3c-0d3e-e5dfcdb0cb88.png)

「C++ クラス」を選択します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/61222/0c7048b3-b094-9d66-07c6-2ccf65d612c0.png)

クラス名を「`ImageView`」、基底クラスを「`QWidget`」に設定します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/61222/1d2e03ef-80b0-5109-630d-8a25acb4c2a2.png)

`imageview.cpp` と `imageview.h` が生成されます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/61222/43550fc0-b7ee-94f9-0ee0-6e753450cc3f.png)

`imageview.h` を開き、`class ImageView : public QWidget` の行にカーソルを移動して、`Alt`+`Enter`を押すとメニューが表示されますので、「基底クラスの仮想関数を追加する」を選択します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/61222/e22eb04a-a9be-d7e1-d5cd-f589be1f71aa.png)

`paintEvent(QPaintEvent *) : void` という関数を追加します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/61222/4b7ab4be-82cc-fefa-3686-246b383c3220.png)

追加した関数名 `paintEvent` の上にカーソルを移動して、`Alt`+`Enter`を押すとメニューが表示されますので、「imageview.cpp に定義を追加」を選択します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/61222/d928ec61-97f2-8387-bee0-70bc243b75e2.png)

`ImageView` クラス内に `QImage image_;` というprivateメンバ変数を追加し、変数名の上にカーソルを移動して、`Alt`+`Enter`を押すとメニューが表示されますので、「Generate Setter」を選択します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/61222/c5c371ae-f4e7-9c3f-51b3-b817d8605300.png)

`ImageView::setImage()` 関数に `update();` という行を追加します。

``` imageview.cpp
void ImageView::setImage(const QImage &newImage)
{
	image_ = newImage;
	update(); // 追加
}
```

`imageview.cpp` にインクルードファイルを追加します

``` imageview.cpp
#include <QPainter>
```

`paintEvent` 関数を実装します。

``` imageview.cpp
void ImageView::paintEvent(QPaintEvent *event)
{
	int x = (width() - image_.width()) / 2;
	int y = (height() - image_.height()) / 2;
	QPainter painter(this);
	painter.drawImage(x, y, image_);
}
```

ここまでで、次のようなコードになっていると思います。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/61222/e7033ec5-9bf6-9573-409a-80933e27e47c.png)

### とりあえずビルドしてみる

`Ctrl`+`B`を押してビルドします。まだ実行はしません。エラーが出たら修正してください。ビルドが成功すると、右下に緑のプログレスバーが表示されます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/61222/a9d1a1e0-0111-34cf-8ae2-ef83937d1985.png)

### メインウィンドウを編集

プロジェクトツリーの「フォーム」から、`mainwindow.ui` をダブルクリックします。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/61222/17daf21b-c4fa-2dde-8db8-84c0a3d2df94.png)

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/61222/5e333096-e6f4-d45d-c8de-ac850cfad9b4.png)

オブジェクトインスペクタの「centralwidget」を右クリックして、「格上げ先を指定...」を選択します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/61222/8dbf5b6a-6c29-0ded-b7d7-bd45f9b0f808.png)

「格上げされたクラス名」の欄に「`ImageView`」と入力し、追加ボタンを押します。さらに「格上げ」ボタンを押します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/61222/73b34109-1523-3516-dc5e-da0ef5a04bd4.png)

左上のメニューバーで、「ここに入力」をダブルクリックして、「`&File`」と入力します。

`File`メニューを開いて、「ここに入力」をダブルクリックし、「`&Open...`」と入力します。

ここまでで、メニューバーができました。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/61222/bdc40fe6-12d1-c58e-c186-d3569136a679.png)

この `Open` メニューが選択された時に実行される関数を実装します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/61222/e02aa4d3-e40c-7b78-b5d4-c7a660652c2b.png)

アクションエディタから、メニュー項目を右クリックして、「スロットへ移動...」を選択します。


![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/61222/ceb8fae8-fa86-57a8-8a92-060393894a02.png)

`triggered()` を選択してOKを押します。

左のツールバーから「編集」ボタンを押して、ソースコード編集画面に移ります。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/61222/eecb67ed-2f5e-b1af-edfd-c075c33788d0.png)

mainwindow.cppを開き、`void MainWindow::on_action_Open_triggered()` 関数を実装します。

``` mainwindow.cpp
void MainWindow::on_action_Open_triggered()
{
	QString path = QFileDialog::getOpenFileName(this, "Open Image");
	if (path.isEmpty()) return;

    QImage image(path);
	ui->centralwidget->setImage(image);
}
```

### 物体検出のコードを実装

前述の通り、推論機能を実装したアプリケーション全体のソースコードは、[筆者のGitHubリポジトリ](https://github.com/soramimi/QtYOLOv9)から取得してください。以下、肝心な部分のみ解説します。

この `on_action_Open_triggered()` 関数の中で、画像を読み込んだら、AIモデルで推論を行い、その結果を描画します。

#### YOLOモデルの準備がまだなら読み込む

``` mainwindow.cpp
	if (!isModelReady()) {
		if (!loadModel()) {
			QMessageBox::critical(this, "Error", "Failed to load model");
			close();
			return;
		}
	}
```

#### 物体検出

``` mainwindow.cpp
	auto bboxes = m->detector.inference(m->image); // inference() は std::optional を返す
```

ここで、`bboxes` は、`std::optional<std::vector<BoundingBox>>` です。`optional`オブジェクトなので、推論が失敗したときは`std::nullopt`、成功したときはバウンディングボックスの`vector`を得られます。

#### 検出結果が有効ならバウンディングボックスを描画する

``` mainwindow.cpp
	if (bboxes) {
		QPainter painter(&m->image);
		painter.setPen(Qt::cyan); // 矩形の色
		painter.setBrush(Qt::NoBrush); // 塗りつぶさない
		for (auto const &bbox : *bboxes) {
			painter.drawRect(bbox.rect); // 矩形を描画
		}
	}
```

### 入力テンソルの作成

最も肝心なコードの実装がまだでした。推論を行う前に、画像から入力テンソルを作成する必要があります。

最初にC++でONNXファイルを読み込むコードを実装したとき、入力テンソルは以下のようなものでした。

``` .cpp
std::vector<float> input_tensor_values(N * C * H * W);
```

これに、画像データを書き込みます。

入力画像を 640 * 640 にリサイズします。

``` .cpp
QImage img = image.convertToFormat(QImage::Format_RGB888).scaled(W, H, Qt::IgnoreAspectRatio, Qt::FastTransformation);
```

画素値を[0, 1]に正規化し、Packed RGB形式からPlanar ~~RGB~~ BGR形式に変換します。

``` .cpp
float *B = input_tensor_values.data() + H * W * 0;
float *G = input_tensor_values.data() + H * W * 1;
float *R = input_tensor_values.data() + H * W * 2;
for (int y = 0; y < H; y++) {
	uint8_t const *src = img.scanLine(y);
	for (int x = 0; x < W; x++) {
		R[W * y + x] = src[3 * x + 0] / 255.0f;
		G[W * y + x] = src[3 * x + 1] / 255.0f;
		B[W * y + x] = src[3 * x + 2] / 255.0f;
	}
}
```

**【2025-01-07訂正】** 入力テンソルはRGBではなくBGRが正しいです。教えてくださった方ありがとうございます！

最初の推論プログラムにこれを組み込み、GUIアプリと統合し、画像ファイルを開いたときに、推論を実行するようにすれば完成です。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/61222/411a57e8-9044-0ecc-8d71-86380e3801b8.png)

https://github.com/soramimi/QtYOLOv9