外接矩形を抽出して対象ワークを認識する【道具としての OpenCV 画像認識】

# 本記事でできること
元画像
![bounding_sample.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/37698/abc20c51-db09-dd1c-5f45-91ef86d796ff.jpeg)


かたまりを矩形で囲んで描画した画像
![Screenshot from 2024-03-10 15-46-25.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/37698/e373a0b1-46ce-6f11-536a-7998a9fc32cb.png)


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

## 外接矩形を抽出して対象ワークを認識する
画像処理フィルターをかけて、認識対象物体を抽出することができたら、次はその対象物体の位置が画像座標内のどこにあって、どのくらいの大きさで見えているのかということを計測したいと思います。本記事では、抽出された対象物体を囲む矩形（＝外接矩形）を描くプログラムを掲載します。

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

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

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

## プログラム抜粋
```c++
std::optional<std::vector<cv::Rect>> get_bounding_rects(const cv::Mat& image) {
    std::vector<std::vector<cv::Point>> contours;
    cv::Mat gray_image = convert_grayscale(image);
    cv::findContours(gray_image, contours, cv::RETR_EXTERNAL, cv::CHAIN_APPROX_SIMPLE);
    cv::Mat input_points;
    std::vector<cv::Rect> rects;
    bool target_is_found = false;
    for (size_t i = 0; i < contours.size(); i++) {
        if (contours[i].size() > 0) {
            cv::Mat(contours[i]).convertTo(input_points, CV_32F);
            cv::Rect result_rect = cv::boundingRect(input_points);
            rects.push_back(result_rect);
            target_is_found = true;
        }
    }
    if (target_is_found == false) {
        return std::nullopt;
    }
    return rects;
}
```

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

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_grayscale(const cv::Mat& image) {
    const int grayscale_channel_num = 1;
    if (image.channels() == grayscale_channel_num) {
        return image.clone();
    }
    cv::Mat gray_image;
    cvtColor(image, gray_image, cv::COLOR_BGR2GRAY);
    return gray_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;
}

std::optional<std::vector<cv::Rect>> get_bounding_rects(const cv::Mat& image) {
    std::vector<std::vector<cv::Point>> contours;
    cv::Mat gray_image = convert_grayscale(image);
    cv::findContours(gray_image, contours, cv::RETR_EXTERNAL, cv::CHAIN_APPROX_SIMPLE);
    cv::Mat input_points;
    std::vector<cv::Rect> rects;
    bool target_is_found = false;
    for (size_t i = 0; i < contours.size(); i++) {
        if (contours[i].size() > 0) {
            cv::Mat(contours[i]).convertTo(input_points, CV_32F);
            cv::Rect result_rect = cv::boundingRect(input_points);
            rects.push_back(result_rect);
            target_is_found = true;
        }
    }
    if (target_is_found == false) {
        return std::nullopt;
    }
    return rects;
}

cv::Mat draw_rectangle(
        const cv::Mat image,
        const cv::Point start,
        const cv::Point finish,
        const cv::Scalar color,
        const int thickness) {
    cv::Mat drawed_image = convert_colorimage(image);
    cv::rectangle(drawed_image, start, finish, color, thickness, cv::LINE_4);
    return drawed_image;
}

cv::Mat draw_rectangle(const cv::Mat image, const cv::Rect rect, const cv::Scalar color, const int thickness) {
    cv::Point start;
    cv::Point finish;
    start.x = rect.x;
    start.y = rect.y;
    finish.x = rect.x + static_cast<int>(rect.width);
    finish.y = rect.y + static_cast<int>(rect.height);
    return draw_rectangle(image, start, finish, color, thickness);
}

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

    std::optional<std::vector<cv::Rect>> maybe_rects = get_bounding_rects(loaded_image);

    // 検出できなかった場合は、プログラムを終了する。
    if (maybe_rects == std::nullopt) {
        std::cout << "There are no objects." << std::endl;
        cv::imshow("window", loaded_image);
        cv::waitKey(0);
        return -1;
    }

    // 検出できた場合には、すべての矩形を描画する。
    const std::vector<cv::Rect> rects = maybe_rects.value();
    cv::Mat result_image = loaded_image.clone();
    for (int i = 0; i < static_cast<int>(rects.size()); i++) {
        result_image = draw_rectangle(result_image, rects[i], cv::Scalar(0, 0, 255), 3);
    }
    cv::imshow("window", result_image);
    cv::waitKey(0);
}
```

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