【C++/Qt/OpenCV】QtとOpenCVでインタラクティブな画像の加工を試す

# はじめに
OpenCVで画像から物体を検出する際、以下の作業を行うことがあります。
- HSV空間に変換してHueの要素から色を検出する
- あるしきい値を基準にバイナリ画像へ変換する

それらのしきい値を変えてはソフトウェアを再実行するのが手間に感じたのでインタラクティブなツールを作ってみようとしました。
以下のようなものが完成イメージです。
左側が元画像、真ん中がHSVに変換された画像、右側がHSVのHueをもとにマスクした画像です。

https://x.com/ko1_baseba11/status/1774064660490375624?s=20

緑のKを認識したいときは、Hueの値は80~111の間になることがわかりました。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/426354/bef515f5-ed54-a52e-fd37-1ccb4450d2bc.png)

# 環境
以下の環境で動作を確認しています。

| 項目 | バージョン |
|:-: | :-:|
| Ubuntu | 22.04 |
| Qt/Qt creator | Qt Creator 6.0.2 based on Qt 5.15.3 |

QtのUIを作成することができるQtCreatorは以下のコマンドでインストールできます。
```shell
sudo apt install qtcreator
```

# QtCreatorの使い方
## プロジェクトの作成
QtCreatorを使うのは初めてだったので簡単に使い方も説明していきます。

以下のコマンドで起動します。
```
qtcreator
```

最初の画面です。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/426354/2933dd26-490e-4a3d-190b-d1c3e8189f81.png)

左側の「プロジェクト」を選択し、ディレクトリ名等を入力して作成するプロジェクトの設定を行っていきます。
今回、ビルドタイプはCMakeを選択しました。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/426354/2c69236a-cbcc-a9c3-eaeb-9672e8a8a642.png)

プロジェクトの設定が完了すると以下のような画面になります。
デフォルトでソースファイル、ヘッダファイル、ui用のファイルが生成されます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/426354/f812c7d5-dca2-9690-ce2c-72cd604b1334.png)

生成されたファイル一覧

```shell
CMakeLists.txt
CMakeLists.txt.user
main.cpp
mainwindow.cpp
mainwindow.h
mainwindow.ui
```

Qtの開発(少なくとも今回は)では主に、 `CMakeLists.txt`, `mainwindow.cpp`. `mainwindow.h`, `mainwindow.ui`を編集します。
また、`mainwindow.ui`はXML形式のファイルですが、専用のUI作成のツールから作成するので直接XML形式のファイルを編集することはほぼありません。

## UIの作成
UIを編集していきます。
上記の画面の状態で左側ファイル一覧の`mainwindow.ui`をダブルクリックするとUIの編集画面が立ち上がります。
この画面の左側から好きな要素をドラッグアンドドロップすることでUIの要素を追加できます。

また、スライドバーのような、ユーザーのインプットがある要素は設定するとコールバック関数のひな型を自動で生成してくれます。
その要素を選択した状態で右クリック=>「スロットへ移動」=>シグナルを選択で、選択された場合や、値が変わった場合などのタイミングで実行される関数のひな型を作成することができます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/426354/5395349e-d5f8-a256-7215-1c88938876c2.png)

`slider_min`という名前のスライドバーで`valueChanged`を選択した場合は以下のようにヘッダファイルとソースファイルに追記されます。便利ですね。

```mainwindow.h
private slots:
    void on_slider_min_valueChanged(int value);
```

```mainwindow.cpp
void MainWindow::on_slider_min_valueChanged(int value)
{

}
```

最終的にUIは以下のようにしました。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/426354/439dfcc2-61ee-c11e-0c1c-3faed7ff6a5a.png)

ソフトウェアの実行は右下の緑色の再生ボタンから実行できます。

# プロジェクトへOpenCVの導入
OpenCVを使いたいので、CMakeLists.txtを編集しましょう。

```cmake:CMakeLists.txt
cmake_minimum_required(VERSION 3.5)

project(interactive_image_processor VERSION 0.1 LANGUAGES CXX)

set(CMAKE_INCLUDE_CURRENT_DIR ON)

set(CMAKE_AUTOUIC ON)
set(CMAKE_AUTOMOC ON)
set(CMAKE_AUTORCC ON)

set(CMAKE_CXX_STANDARD 11)
set(CMAKE_CXX_STANDARD_REQUIRED ON)

find_package(QT NAMES Qt6 Qt5 COMPONENTS Widgets REQUIRED)
find_package(Qt${QT_VERSION_MAJOR} COMPONENTS Widgets REQUIRED)
# 追加
find_package(OpenCV REQUIRED)

# 追加
include_directories(
 ${OpenCV_INCLUDE_DIRS} 
)

set(PROJECT_SOURCES
        main.cpp
        mainwindow.cpp
        mainwindow.h
        mainwindow.ui
)

if(${QT_VERSION_MAJOR} GREATER_EQUAL 6)
    qt_add_executable(interactive_image_processor
        MANUAL_FINALIZATION
        ${PROJECT_SOURCES}
    )
# Define target properties for Android with Qt 6 as:
#    set_property(TARGET interactive_image_processor APPEND PROPERTY QT_ANDROID_PACKAGE_SOURCE_DIR
#                 ${CMAKE_CURRENT_SOURCE_DIR}/android)
# For more information, see https://doc.qt.io/qt-6/qt-add-executable.html#target-creation
else()
    if(ANDROID)
        add_library(interactive_image_processor SHARED
            ${PROJECT_SOURCES}
        )
# Define properties for Android with Qt 5 after find_package() calls as:
#    set(ANDROID_PACKAGE_SOURCE_DIR "${CMAKE_CURRENT_SOURCE_DIR}/android")
    else()
        add_executable(interactive_image_processor
            ${PROJECT_SOURCES}
        )
    endif()
endif()

# ${OpenCV_LIBRARIES}を追加
target_link_libraries(interactive_image_processor PRIVATE Qt${QT_VERSION_MAJOR}::Widgets ${OpenCV_LIBRARIES})

set_target_properties(interactive_image_processor PROPERTIES
    MACOSX_BUNDLE_GUI_IDENTIFIER my.example.com
    MACOSX_BUNDLE_BUNDLE_VERSION ${PROJECT_VERSION}
    MACOSX_BUNDLE_SHORT_VERSION_STRING ${PROJECT_VERSION_MAJOR}.${PROJECT_VERSION_MINOR}
    MACOSX_BUNDLE TRUE
    WIN32_EXECUTABLE TRUE
)

if(QT_VERSION_MAJOR EQUAL 6)
    qt_finalize_executable(interactive_image_processor)
endif()
```

ヘッダファイルにも以下を追加しておきます。

```cpp
#include <opencv2/opencv.hpp>
```


# ソースコード
最終的に、`mainwindow.cpp`は以下のようになりました。

```mainwindow.cpp
#include "mainwindow.h"
#include "./ui_mainwindow.h"

MainWindow::MainWindow(QWidget *parent)
    : QMainWindow(parent)
    , ui(new Ui::MainWindow)
{
    ui->setupUi(this);
    original_image_ = cv::imread("PATH_TO_FILE");

    int height = ui->image_label_1->height();
    int big_height = original_image_.cols > original_image_.rows ? original_image_.cols : original_image_.rows;
    double ratio = ((double)height / (double)big_height);

    cv::resize(original_image_, original_image_, cv::Size(), ratio, ratio, cv::INTER_NEAREST);
    ui->image_label_1->setPixmap(QPixmap::fromImage(QImage((unsigned char*) original_image_.data, original_image_.cols, original_image_.rows, original_image_.step, QImage::Format_BGR888)));

    cv::cvtColor(original_image_, mat_, cv::COLOR_BGR2HSV_FULL);
    ui->image_label_2->setPixmap(QPixmap::fromImage(QImage((unsigned char*) mat_.data, mat_.cols, mat_.rows, mat_.step, QImage::Format_RGB888)));

    min_hue_ = 0;
    max_hue_ = 255;
}

MainWindow::~MainWindow()
{
    delete ui;
}

void MainWindow::on_slider_min_valueChanged(int value)
{
    min_hue_ = value;
    mask_by_hue(mat_, mask_, min_hue_, max_hue_);
    ui->image_label_3->setPixmap(QPixmap::fromImage(QImage((unsigned char*) mask_.data, mask_.cols, mask_.rows, mask_.step, QImage::Format_Indexed8)));
    ui->label_hue_min_value->setText(QString::number(value));
}

void MainWindow::on_slider_max_valueChanged(int value)
{
    max_hue_ = value;
    mask_by_hue(mat_, mask_, min_hue_, max_hue_);
    ui->image_label_3->setPixmap(QPixmap::fromImage(QImage((unsigned char*) mask_.data, mask_.cols, mask_.rows, mask_.step, QImage::Format_Indexed8)));
    ui->label_hue_max_value->setText(QString::number(value));
}

void MainWindow::mask_by_hue(cv::Mat& hsv_img, cv::Mat& mask, int min_hue, int max_hue) {
    cv::inRange(hsv_img, cv::Scalar(min_hue, 50, 0), cv::Scalar(max_hue, 255, 255), mask);
}
```

ヘッダファイルは以下です。

```mainwindow.h
#ifndef MAINWINDOW_H
#define MAINWINDOW_H

#include <QMainWindow>
#include <opencv2/opencv.hpp>

#include <QPixmap>
#include <QString>


QT_BEGIN_NAMESPACE
namespace Ui { class MainWindow; }
QT_END_NAMESPACE

class MainWindow : public QMainWindow
{
    Q_OBJECT

public:
    MainWindow(QWidget *parent = nullptr);
    ~MainWindow();

private slots:
    void on_slider_min_valueChanged(int value);
    void on_slider_max_valueChanged(int value);

private:
    Ui::MainWindow *ui;
    cv::Mat original_image_, mat_, mask_;
    int min_hue_, max_hue_;
    void mask_by_hue(cv::Mat& hsv_img, cv::Mat& mask, int min_hue, int max_hue);
};
#endif // MAINWINDOW_H
```


それぞれのスライドバーのコールバック関数内でマスク処理を呼び出して画像とその時のHueの値を更新しています。

```cpp
void MainWindow::on_slider_min_valueChanged(int value)
{
    min_hue_ = value;
    mask_by_hue(mat_, mask_, min_hue_, max_hue_);
    ui->image_label_3->setPixmap(QPixmap::fromImage(QImage((unsigned char*) mask_.data, mask_.cols, mask_.rows, mask_.step, QImage::Format_Indexed8)));
    ui->label_hue_min_value->setText(QString::number(value));
}

void MainWindow::on_slider_max_valueChanged(int value)
{
    max_hue_ = value;
    mask_by_hue(mat_, mask_, min_hue_, max_hue_);
    ui->image_label_3->setPixmap(QPixmap::fromImage(QImage((unsigned char*) mask_.data, mask_.cols, mask_.rows, mask_.step, QImage::Format_Indexed8)));
    ui->label_hue_max_value->setText(QString::number(value));
}
```

# おわりに
今回は画像処理のための簡単なツールを作ってみました。自分の手間をなくす用にとりあえず作ってみたものですが、今後もっと拡張していきたいです。

今のところ以下を検討しています。
- 画像/動画ファイルの読み込み
- 他の画像処理方法の追加

# 参考
OpenCV×Qtについて

https://qiita.com/hoshianaaa/items/31888e3fa8fba7dff344

QtCreatorの使い方について

https://aquarius-train.hatenablog.com/entry/Qt%E3%81%A8QtCreator%E3%82%92Ubuntu%E3%81%AB%E3%82%A4%E3%83%B3%E3%82%B9%E3%83%88%E3%83%BC%E3%83%AB%E3%81%99%E3%82%8B%E6%89%8B%E9%A0%86%E3%81%A8%E4%BD%BF%E3%81%84%E6%96%B9%E3%82%92%E8%A7%A3%E8%AA%AC