画像に文字を書く【道具としての OpenCV 画像認識】

# 本記事でできること
![Screenshot from 2024-03-10 12-18-41.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/37698/8bd96ce4-8509-f3a0-9150-eba6cbbd4416.png)


拡大
![Screenshot from 2024-03-10 12-18-52.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/37698/ed116213-ab24-ffa4-a977-ff2f592f33d1.png)


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

## 画像に文字を書く
ロボットのビジュアルフィードバック制御のためのプログラムにおいて、カメラ画像のなかに処理の結果や判定した数値などの情報を追加表示したい場合があります。本記事では、画像の中に文字を表示するためのプログラムを掲載します。

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

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

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

## プログラム抜粋
```c++
cv::Mat overlay_image_on_image(
        const cv::Mat& target_image, const cv::Point draw_point, const cv::Mat& overlay_image, const cv::Point offset) {
    cv::Mat result_image = target_image.clone();
    const bool is_draw_point_inside_target_image = is_point_inside_image(result_image, draw_point);
    if (is_draw_point_inside_target_image == false) {
        return result_image;
    }
    cv::Rect overlay_roi(draw_point + offset, cv::Size(overlay_image.cols, overlay_image.rows));
    if (overlay_roi.br().x > result_image.cols) {  // 重ねる画像右にはみ出す場合は注目領域の左にテキストを配置します。
        overlay_roi.x -= overlay_image.cols + 2 * offset.x;
    }
    if (overlay_roi.br().y > result_image.rows) {  // 重ねる画像が下にはみ出す場合は注目領域の上にテキストを配置します。
        overlay_roi.y -= overlay_image.rows + 2 * offset.y;
    }

    cv::Mat overlaid_image = result_image(overlay_roi);
    cv::addWeighted(overlaid_image, 0.5, overlay_image, 0.5, 0, overlaid_image);
    return result_image;
}

cv::Mat create_text_image(const std::string message) {
    const auto font = cv::HersheyFonts::FONT_HERSHEY_COMPLEX;
    const cv::Scalar font_color = {0, 0, 0};
    const auto quality = cv::LineTypes::LINE_AA;
    const int message_word_count = static_cast<int>(message.size());
    cv::Mat text_image(20, 10 * message_word_count, CV_8UC3, cv::Scalar{255, 255, 255});
    cv::putText(text_image, message, {2, 14}, font, 0.5, font_color, 1, quality);
    return text_image;
}

cv::Mat draw_text_on_image(const cv::Mat& image, const cv::Point draw_point, const std::string message) {
    const cv::Mat text_image = create_text_image(message);
    cv::Mat result_image = overlay_image_on_image(image, draw_point, text_image, cv::Point(10, 10));
    return result_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;
}

bool is_point_inside_image(const cv::Mat& image, const cv::Point point) {
    bool is_point_x_inside_image = (0 < point.x && point.x < image.cols);
    bool is_point_y_inside_image = (0 < point.y && point.y < image.rows);
    if (is_point_x_inside_image && is_point_y_inside_image) {
        return true;
    }
    return false;
}

cv::Mat overlay_image_on_image(
        const cv::Mat& target_image, const cv::Point draw_point, const cv::Mat& overlay_image, const cv::Point offset) {
    cv::Mat result_image = target_image.clone();
    const bool is_draw_point_inside_target_image = is_point_inside_image(result_image, draw_point);
    if (is_draw_point_inside_target_image == false) {
        return result_image;
    }
    cv::Rect overlay_roi(draw_point + offset, cv::Size(overlay_image.cols, overlay_image.rows));
    if (overlay_roi.br().x > result_image.cols) {  // 重ねる画像右にはみ出す場合は注目領域の左にテキストを配置します。
        overlay_roi.x -= overlay_image.cols + 2 * offset.x;
    }
    if (overlay_roi.br().y > result_image.rows) {  // 重ねる画像が下にはみ出す場合は注目領域の上にテキストを配置します。
        overlay_roi.y -= overlay_image.rows + 2 * offset.y;
    }

    cv::Mat overlaid_image = result_image(overlay_roi);
    cv::addWeighted(overlaid_image, 0.5, overlay_image, 0.5, 0, overlaid_image);
    return result_image;
}

cv::Mat create_text_image(const std::string message) {
    const auto font = cv::HersheyFonts::FONT_HERSHEY_COMPLEX;
    const cv::Scalar font_color = {0, 0, 0};
    const auto quality = cv::LineTypes::LINE_AA;
    const int message_word_count = static_cast<int>(message.size());
    cv::Mat text_image(20, 10 * message_word_count, CV_8UC3, cv::Scalar{255, 255, 255});
    cv::putText(text_image, message, {2, 14}, font, 0.5, font_color, 1, quality);
    return text_image;
}

cv::Mat draw_text_on_image(const cv::Mat& image, const cv::Point draw_point, const std::string message) {
    const cv::Mat text_image = create_text_image(message);
    cv::Mat result_image = overlay_image_on_image(image, draw_point, text_image, cv::Point(10, 10));
    return result_image;
}

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

    const cv::Point text_start_point(300, 140);
    const std::string message = "FUJISAN";
    const cv::Mat image_with_text_pverlay = draw_text_on_image(loaded_image, text_start_point, message);

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

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