照明の影に影響されない２値化【道具としての OpenCV 画像認識】

# 本記事でできること
元画像（影がかかっている）
![green_sample_with_shadow.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/37698/e3ccc0b2-cac0-7dbb-deb8-9e47f782881d.jpeg)


普通の２値化（影を除去できていない）
![Screenshot from 2024-03-10 17-46-00.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/37698/1d681a52-dcc2-a2e8-0ef1-4bedcc551535.png)


適応的２値化（影の影響を受けずに２値化する）
![Screenshot from 2024-03-10 17-46-45.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/37698/0a83b5c5-4343-4f4d-1377-cf52264412e1.png)

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

## 照明の影に影響されない２値化
画像認識プログラムを制作しているとき、照明環境やロボットの姿勢、認識対象物の位置などによって、影ができてしまい、明るさのムラが撮像したカメラ画像に現れることがあります。この影がある画像では、普通の２値化をかけても除去することができず、撮像後のカメラ画像内に一律で明るさ調整をかけても除去することが難しいです。そこで、適応的２値化という単純な明るさだけではなく、輪郭の情報を併用する２値化手法を用いることがあります。本記事では適応的２値化のプログラムを掲載します。


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

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

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

## プログラム抜粋
```c++
cv::Mat adaptive_binary(const cv::Mat& image, const int block_size, const int param_c) {
    cv::Mat image_gray = convert_grayscale(image);
    cv::Mat binary_image;
    int input_block_size = block_size;
    if (input_block_size < 3) {
        input_block_size = 3;
    }
    if (input_block_size % 2 == 0) {
        input_block_size = block_size + 1;
    }
    cv::adaptiveThreshold(
            image_gray,
            binary_image,
            255,
            cv::ADAPTIVE_THRESH_GAUSSIAN_C,
            cv::THRESH_BINARY,
            input_block_size,
            param_c);
    return binary_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_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 binary(const cv::Mat& image, const int threthold) {
    cv::Mat image_gray = convert_grayscale(image);
    cv::Mat binary_image;
    if (threthold == 0) {
        threshold(image_gray, binary_image, 0, 255, cv::THRESH_BINARY | cv::THRESH_OTSU);
    } else {
        threshold(image_gray, binary_image, threthold, 255, cv::THRESH_BINARY);
    }
    return binary_image;
}

cv::Mat adaptive_binary(const cv::Mat& image, const int block_size, const int param_c) {
    cv::Mat image_gray = convert_grayscale(image);
    cv::Mat binary_image;
    int input_block_size = block_size;
    if (input_block_size < 3) {
        input_block_size = 3;
    }
    if (input_block_size % 2 == 0) {
        input_block_size = block_size + 1;
    }
    cv::adaptiveThreshold(
            image_gray,
            binary_image,
            255,
            cv::ADAPTIVE_THRESH_GAUSSIAN_C,
            cv::THRESH_BINARY,
            input_block_size,
            param_c);
    return binary_image;
}

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

    cv::Mat binary_image = binary(loaded_image, 0);
    cv::imshow("window", binary_image);

    cv::Mat adaptive_binary_image = adaptive_binary(loaded_image, 50, 10);
    cv::imshow("window", adaptive_binary_image);
}
```


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