画像から特定の色を抜き出す【道具としての OpenCV 画像認識】

# 本記事でできること
元画像
![Screenshot from 2024-03-03 19-44-05.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/37698/50b370ba-25b4-f9c0-6884-1df6dfd88ff4.png)


色（緑）を抽出した画像
![Screenshot from 2024-03-03 21-38-02.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/37698/2058c909-dfa1-b61b-aaaa-947cdbb8111a.png)


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

## 画像から特定の色を抜き出す
ビジュアルフィードバック制御でハンドリング対象物体を認識するときや、背景を除去する場合などで色情報に基づいて、特定の色の部分を抜き出したり除去したりすることがあります。本記事では画像の色に基づいて抜き出すプログラムを掲載します。

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

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

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

## プログラム抜粋
```c++
/*
 * @param[in] extract_value
 * チャンネル1下限、チャンネル1上限、チャンネル2下限、チャンネル2上限、チャンネル3下限、チャンネル3上限
 * チャンネル1: 0~180, チャンネル2: 0,255, チャンネル3: 0~255
 */
cv::Mat extract_by_color(const cv::Mat& image, const int extract_value[6], const int cv_color_code) {
    cv::Mat object_img = convert_colorimage(image);
    cv::Mat extracted_image;
    cv::Mat colored_image;

    cv::Mat lut = cv::Mat(256, 1, CV_8UC3);
    int lower[3] = {extract_value[0], extract_value[2], extract_value[4]};
    int upper[3] = {extract_value[1], extract_value[3], extract_value[5]};

    cv::cvtColor(object_img, colored_image, cv_color_code);

    for (int i = 0; i < 256; i++) {
        for (int k = 0; k < 3; k++) {
            if (lower[k] <= upper[k]) {
                if ((lower[k] <= i) && (i <= upper[k])) {
                    lut.data[i * lut.step + k] = 255;
                } else {
                    lut.data[i * lut.step + k] = 0;
                }
            } else {
                if ((i <= upper[k]) || (lower[k] <= i)) {
                    lut.data[i * lut.step + k] = 255;
                } else {
                    lut.data[i * lut.step + k] = 0;
                }
            }
        }
    }

    // LUTを使用して二値化、Channel毎に分解
    cv::LUT(colored_image, lut, colored_image);
    std::vector<cv::Mat> planes;
    cv::split(colored_image, planes);

    // マスクを作成
    cv::Mat mask_image;
    cv::bitwise_and(planes[0], planes[1], mask_image);
    cv::bitwise_and(mask_image, planes[2], mask_image);

    object_img.copyTo(extracted_image, mask_image);
    return extracted_image;
}
```

## 全体プログラム
```c++
#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;
}

/*
 * @param[in] extract_value
 * チャンネル1下限、チャンネル1上限、チャンネル2下限、チャンネル2上限、チャンネル3下限、チャンネル3上限
 * チャンネル1: 0~180, チャンネル2: 0,255, チャンネル3: 0~255
 */
cv::Mat extract_by_color(const cv::Mat& image, const int extract_value[6], const int cv_color_code) {
    cv::Mat object_img = convert_colorimage(image);
    cv::Mat extracted_image;
    cv::Mat colored_image;

    cv::Mat lut = cv::Mat(256, 1, CV_8UC3);
    int lower[3] = {extract_value[0], extract_value[2], extract_value[4]};
    int upper[3] = {extract_value[1], extract_value[3], extract_value[5]};

    cv::cvtColor(object_img, colored_image, cv_color_code);

    for (int i = 0; i < 256; i++) {
        for (int k = 0; k < 3; k++) {
            if (lower[k] <= upper[k]) {
                if ((lower[k] <= i) && (i <= upper[k])) {
                    lut.data[i * lut.step + k] = 255;
                } else {
                    lut.data[i * lut.step + k] = 0;
                }
            } else {
                if ((i <= upper[k]) || (lower[k] <= i)) {
                    lut.data[i * lut.step + k] = 255;
                } else {
                    lut.data[i * lut.step + k] = 0;
                }
            }
        }
    }

    // LUTを使用して二値化、Channel毎に分解
    cv::LUT(colored_image, lut, colored_image);
    std::vector<cv::Mat> planes;
    cv::split(colored_image, planes);

    // マスクを作成
    cv::Mat mask_image;
    cv::bitwise_and(planes[0], planes[1], mask_image);
    cv::bitwise_and(mask_image, planes[2], mask_image);

    object_img.copyTo(extracted_image, mask_image);
    return extracted_image;
}

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

    const int extract_value[6] = {40, 100, 0, 255, 0, 180};
    const cv::Mat color_extracted_image = extract_by_color(loaded_image, extract_value, cv::COLOR_BGR2HSV);

    cv::imshow("window", color_extracted_image);
    cv::waitKey(0);
}
```

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