画像から円環対応のHSV形式で色を抜き出す【道具としての OpenCV 画像認識】

# 本記事でできること
元画像
![green_sample.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/37698/df143b28-a5a3-aa52-c689-0d9344532410.jpeg)

赤色を抽出した画像
![Screenshot from 2024-03-06 20-35-42.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/37698/9f355a36-c875-b3d8-f459-a4f7d172e325.png)



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

## 画像から円環対応のHSV形式で色を抜き出す
色情報を用いて認識対象となるワークを抽出するとき、HSV表色系で色情報を指定する場合が多くあります。OpenCVのHSV表色系では、色相を0〜180の数値を用いて指定しますが、0と180の間（赤っぽい色）を指定して抜き出したいケースがあります。すなわち、色相環を用いて色を指定する場合です。本記事では、画像から円環対応のHSV形式で色を抜き出すプログラムを掲載します。


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

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

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

## プログラム抜粋
```c++
cv::Mat extract_by_hsv_color(const cv::Mat& image, const hsvParam hsv_param) {
    const int hue_lower_limit = 0;
    const int hue_upper_limit = 180;
    if (hsv_param.hue_start < hsv_param.hue_finish) {
        int extract_value[6] = {
                hsv_param.hue_start,
                hsv_param.hue_finish,
                hsv_param.saturation_lower,
                hsv_param.saturation_upper,
                hsv_param.value_lower,
                hsv_param.value_upper};
        cv::Mat extracted_image = extract_by_color(image, extract_value, cv::COLOR_BGR2HSV);
        return extracted_image;
    } else {
        int extract_value_start[6] = {
                hsv_param.hue_start,
                hue_upper_limit,
                hsv_param.saturation_lower,
                hsv_param.saturation_upper,
                hsv_param.value_lower,
                hsv_param.value_upper};
        cv::Mat extracted_image_start = extract_by_color(image, extract_value_start, cv::COLOR_BGR2HSV);
        int extract_value_finish[6] = {
                hue_lower_limit,
                hsv_param.hue_finish,
                hsv_param.saturation_lower,
                hsv_param.saturation_upper,
                hsv_param.value_lower,
                hsv_param.value_upper};
        cv::Mat extracted_image_finish = extract_by_color(image, extract_value_finish, cv::COLOR_BGR2HSV);
        cv::Mat extracted_image = extracted_image_start + extracted_image_finish;
        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;
}

struct hsvParam {
    int hue_start;
    int hue_finish;
    int saturation_lower;
    int saturation_upper;
    int value_lower;
    int value_upper;
};

/**
 * hue_start 色相値始点(0~180)
 * hue_finish 色相値終点(0~180)
 * saturation_lower 彩度値始点(0~255)
 * saturation_upper 彩度値終点(0~255)
 * value_lower 明度値始点(0~255)
 * value_upper 明度値終点(0~255)
 */
hsvParam set_hsv_param(
        const int hue_start,
        const int hue_finish,
        const int saturation_lower,
        const int saturation_upper,
        const int value_lower,
        const int value_upper) {
    return hsvParam{hue_start, hue_finish, saturation_lower, saturation_upper, value_lower, value_upper};
}

cv::Mat extract_by_hsv_color(const cv::Mat& image, const hsvParam hsv_param) {
    const int hue_lower_limit = 0;
    const int hue_upper_limit = 180;
    if (hsv_param.hue_start < hsv_param.hue_finish) {
        int extract_value[6] = {
                hsv_param.hue_start,
                hsv_param.hue_finish,
                hsv_param.saturation_lower,
                hsv_param.saturation_upper,
                hsv_param.value_lower,
                hsv_param.value_upper};
        cv::Mat extracted_image = extract_by_color(image, extract_value, cv::COLOR_BGR2HSV);
        return extracted_image;
    } else {
        int extract_value_start[6] = {
                hsv_param.hue_start,
                hue_upper_limit,
                hsv_param.saturation_lower,
                hsv_param.saturation_upper,
                hsv_param.value_lower,
                hsv_param.value_upper};
        cv::Mat extracted_image_start = extract_by_color(image, extract_value_start, cv::COLOR_BGR2HSV);
        int extract_value_finish[6] = {
                hue_lower_limit,
                hsv_param.hue_finish,
                hsv_param.saturation_lower,
                hsv_param.saturation_upper,
                hsv_param.value_lower,
                hsv_param.value_upper};
        cv::Mat extracted_image_finish = extract_by_color(image, extract_value_finish, cv::COLOR_BGR2HSV);
        cv::Mat extracted_image = extracted_image_start + extracted_image_finish;
        return extracted_image;
    }
}

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

    hsvParam hsv_param = set_hsv_param(175, 5, 100, 255, 0, 255);
    const cv::Mat color_extracted_image = extract_by_hsv_color(loaded_image, hsv_param);

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

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