マウス操作で画像情報を得る【道具としての OpenCV 画像認識】

# 本記事でできること
![Screenshot from 2024-03-10 13-13-37.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/37698/2cfa7ac5-6a99-388d-741f-5b71b746d84c.png)


# 想定シーン
## ロボットをビジュアルフィードバック制御する
カメラを使ったロボットアームの制御方法を、ビジュアルフィードバック制御といいます。ここでは、ビジュアルフィードバック制御を使った自動化システムを開発する際に必要となる画像認識プログラムをまとめています。

## マウス操作で画像情報を得る
カメラ画像の特定の領域を色や明るさで抜き出したいと考えたとき、そもそも色味や明るさはどのくらいなのかすぐに知りたいという場合があります。パラメータを調整して数値を推定していくよりも特定の画素を指定して情報を取得できることが便利なケースもあります。本記事では、マウスで画像内の特定の箇所をクリックして、画像情報を得るためのプログラムを掲載します。

# 実行環境
- OS：Ubuntu 22.04 LTS
- 言語：C++
- クルーボ：v5.1.0

## クルーボについて
ロボットアプリケーション開発には、株式会社チトセロボティクスのロボット制御ソフトウェア「クルーボ」を使用します。本記事のプログラムは、クルーボがインストールされた制御コンピュータ上で動作します。

- クルーボの製品サイト：https://chitose-robotics.com/

## プログラム抜粋
```c++
void sampling_with_mouse(const cv::Mat& image) {
    mouseParam mouse_param;
    cv::String window_name = "sampling_with_mouse";
    cv::imshow(window_name, image);
    cv::setMouseCallback(window_name, mouseCallBackFunc, &mouse_param);
    bool is_mouse_l_button_down = false;
    cv::Point mouse_point(0, 0);

    std::cout << "'q'キーで終了します。" << std::endl;
    while (cv::waitKey(1) != 'q') {
        mouse_point = cv::Point(mouse_param.x, mouse_param.y);
        const cv::Mat sample_image = draw_dot(image, mouse_point, 10, cv::Scalar(255, 255, 255));

        if (mouse_param.event == cv::EVENT_LBUTTONDOWN) {
            is_mouse_l_button_down = true;
        }
        if (is_mouse_l_button_down && (mouse_param.event == cv::EVENT_LBUTTONUP)) {
            cv::Mat color_image = convert_colorimage(image);
            cv::Mat hsv_image;
            cv::cvtColor(color_image, hsv_image, cv::COLOR_BGR2HSV);
            fmt::print(
                    "XY = ({:4}, {:4}) | BGR = ({:4}, {:4}, {:4}) | HSV = ({:4}, {:4}, {:4})\n",
                    mouse_param.x,
                    mouse_param.y,
                    static_cast<int>(color_image.at<cv::Vec3b>(mouse_param.y, mouse_param.x)[0]),
                    static_cast<int>(color_image.at<cv::Vec3b>(mouse_param.y, mouse_param.x)[1]),
                    static_cast<int>(color_image.at<cv::Vec3b>(mouse_param.y, mouse_param.x)[2]),
                    static_cast<int>(hsv_image.at<cv::Vec3b>(mouse_param.y, mouse_param.x)[0]),
                    static_cast<int>(hsv_image.at<cv::Vec3b>(mouse_param.y, mouse_param.x)[1]),
                    static_cast<int>(hsv_image.at<cv::Vec3b>(mouse_param.y, mouse_param.x)[2]));
            is_mouse_l_button_down = false;
        }

        cv::imshow(window_name, sample_image);
    }
}
```

## 全体プログラム
```c++
#include <fmt/chrono.h>
#include <iostream>
#include <opencv2/opencv.hpp>

cv::Mat load_image(const std::string file_path) {
    cv::Mat loaded_image = cv::imread(file_path, 1);
    if (loaded_image.empty()) {
        throw std::runtime_error("画像を正常に読み込めませんでした。");
    }
    return loaded_image;
}

cv::Mat convert_colorimage(const cv::Mat& image) {
    const int colorimage_channel_num = 3;
    if (image.channels() == colorimage_channel_num) {
        return image.clone();
    }
    cv::Mat color_image;
    cvtColor(image, color_image, cv::COLOR_GRAY2BGR);
    return color_image;
}

cv::Mat draw_dot(const cv::Mat image, const cv::Point center, const float radius, const cv::Scalar color) {
    cv::Mat drawed_image = convert_colorimage(image);
    const int thickness = -1;
    cv::circle(drawed_image, center, static_cast<int>(radius), color, thickness, CV_MSA);
    return drawed_image;
}

struct mouseParam {
    int x;
    int y;
    int event;
    int flags;
};

void mouseCallBackFunc(int eventType, int x, int y, int flags, void* userdata) {
    mouseParam* ptr = static_cast<mouseParam*>(userdata);
    ptr->x = x;
    ptr->y = y;
    ptr->event = eventType;
    ptr->flags = flags;
}

void sampling_with_mouse(const cv::Mat& image) {
    mouseParam mouse_param;
    cv::String window_name = "sampling_with_mouse";
    cv::imshow(window_name, image);
    cv::setMouseCallback(window_name, mouseCallBackFunc, &mouse_param);
    bool is_mouse_l_button_down = false;
    cv::Point mouse_point(0, 0);

    std::cout << "'q'キーで終了します。" << std::endl;
    while (cv::waitKey(1) != 'q') {
        mouse_point = cv::Point(mouse_param.x, mouse_param.y);
        const cv::Mat sample_image = draw_dot(image, mouse_point, 10, cv::Scalar(255, 255, 255));

        if (mouse_param.event == cv::EVENT_LBUTTONDOWN) {
            is_mouse_l_button_down = true;
        }
        if (is_mouse_l_button_down && (mouse_param.event == cv::EVENT_LBUTTONUP)) {
            cv::Mat color_image = convert_colorimage(image);
            cv::Mat hsv_image;
            cv::cvtColor(color_image, hsv_image, cv::COLOR_BGR2HSV);
            fmt::print(
                    "XY = ({:4}, {:4}) | BGR = ({:4}, {:4}, {:4}) | HSV = ({:4}, {:4}, {:4})\n",
                    mouse_param.x,
                    mouse_param.y,
                    static_cast<int>(color_image.at<cv::Vec3b>(mouse_param.y, mouse_param.x)[0]),
                    static_cast<int>(color_image.at<cv::Vec3b>(mouse_param.y, mouse_param.x)[1]),
                    static_cast<int>(color_image.at<cv::Vec3b>(mouse_param.y, mouse_param.x)[2]),
                    static_cast<int>(hsv_image.at<cv::Vec3b>(mouse_param.y, mouse_param.x)[0]),
                    static_cast<int>(hsv_image.at<cv::Vec3b>(mouse_param.y, mouse_param.x)[1]),
                    static_cast<int>(hsv_image.at<cv::Vec3b>(mouse_param.y, mouse_param.x)[2]));
            is_mouse_l_button_down = false;
        }

        cv::imshow(window_name, sample_image);
    }
}

int main(void) {
    std::string file_path = "../data/fuji.jpg";
    const cv::Mat loaded_image = load_image(file_path);
    sampling_with_mouse(loaded_image);
}
```

### 実行結果
```terminal
XY = ( 340,  140) | BGR = ( 255,  157,   73) | HSV = ( 106,  182,  255)
XY = ( 574,  576) | BGR = (   0,    0,    0) | HSV = (   0,    0,    0)
XY = ( 804,  467) | BGR = ( 132,   88,   64) | HSV = ( 109,  131,  132)
```

## おわりに
人手作業をロボットアームで自動化するために、カメラを使ったロボット制御＝ビジュアルフィードバック制御が大切です。
ロボット制御用の画像認識でも中身のひとつひとつはシンプルなので、要素に分解して解説していきたいと思います。