はじめてのG-API（前編）

## はじめに
* この記事は[OpenCV Advent Calendar 2018](http://qiita.com/advent-calendar/2018/opencv)の16日目の記事です．
* 他の記事は[目次](https://qiita.com/advent-calendar/2018/opencv)にまとめられています．

この記事では，OpenCV 4.0から追加された[G-API](https://github.com/opencv/opencv/wiki/Graph-API)の概要を紹介します．

## 動作確認環境
筆者は以下の環境で動作確認しました．

### OS
- Windows 10 Pro 64bit

### CMake
- 3.13.1

### コンパイラ
- Visual Studio 2017

### OpenCV
- [OpenCV 4.0.0](https://github.com/opencv/opencv/releases/tag/4.0.0)

## G-APIとは
画像処理，CVアルゴリズムを処理内容とデータフローを連結グラフとして記述するためのAPI，[Graph-API](https://github.com/opencv/opencv/wiki/Graph-API)（以降はG-APIと表記）がOpenCV 4.0から追加されました．これらの機能は[gapiモジュール](https://github.com/opencv/opencv/tree/4.0.0/modules/gapi)に入っています．

画像処理，CVアルゴリズムをグラフで記述するメリットは以下の通りです．

- 高速化
  - あらかじめ処理内容とデータフローがわかっているので，効率のよい処理（データフロー最適化，並列化），メモリアクセス（タイリングなど）に置き換えることができる（こともある）
- 省メモリ化
  - 余分な中間データのメモリ確保をしなくてよくなり，メモリ消費量が削減できる（こともある）
  - [Graph-API](https://github.com/opencv/opencv/wiki/Graph-API)には`e.g. a 1.5GB image processing pipeline fits into 750KB memory footprint with G-API/Fluid. `という記述がある
- ソースコードの可読性
  - グラフで記述することにより処理内容とデータフローが把握しやすくなる
- プロトタイピングGUIツールとの親和性
  - GUIで処理内容とデータフローをポチポチ作る開発ツール（[Vision Algorithm Designer](https://software.intel.com/en-us/sample-auto-contrast-using-vision-algorithm-designer-for-openvx-graphs)みたいなの）と相性が良い

### G-APIサンプルコード
導入はこのあたりにして，実際のサンプルコードを読みながら，雰囲気を掴んでみましょう．
ここでは，`src-->[グレースケール化]-->[blur]-->[エッジ抽出]-->dst`という処理を例にしてみます．

この処理を従来のOpenCV APIで記述すると以下のようになります．

```c++
cv::Mat src, dst;
cv::Mat gray, blurred;
for (;;)
{
    cap >> src;
    if (src.empty()) break;

    // 一連の処理を実行
    cv::cvtColor(src, gray, cv::COLOR_BGR2GRAY);
    cv::blur(gray, blurred, cv::Size(5, 5));
    cv::Canny(blurred, dst, 32, 128, 3);
}
```

このような記述だと

1. `src`の画像全体をグレースケール化して`gray`に格納
2. `gray`の画像全体にblurを適用したものを`blurred`に格納
3. `blurred`の画像全体にエッジ抽出したものを`dst`に格納

というように処理ごとにメモリ確保，メモリアクセス，処理が行ったり来たりしており無駄が多いことがわかります．一方，G-APIで記述すると以下のようになります． 

```c++
// グラフ定義（処理内容とデータフローをグラフで記述）
cv::GMat in;
cv::GMat gray    = cv::gapi::BGR2Gray(in);
cv::GMat blurred = cv::gapi::blur(gray, cv::Size(5, 5));
cv::GMat out     = cv::gapi::Canny(blurred, 32, 128, 3);
cv::GComputation ac(in, out);

cv::Mat src, dst;
for (;;)
{
    cap >> src;

    // グラフ実行
    ac.apply(src, dst);
}
```

（この例だと処理が短すぎて恩恵がわかりにくいかもしれませんが）処理内容とデータフローをグラフで記述することで処理の流れが追いやすくなっています．また，[Understanding the graph structure](https://docs.opencv.org/4.0.0/d3/d7a/tutorial_gapi_anisotropic_segmentation.html#gapi_anisotropic_inspect_graph)によると[Graphviz](https://www.graphviz.org/)を利用して以下のようにグラフを可視化できるみたいです（[Understanding the graph structure](https://docs.opencv.org/4.0.0/d3/d7a/tutorial_gapi_anisotropic_segmentation.html#gapi_anisotropic_inspect_graph)より引用）．

![](https://docs.opencv.org/4.0.0/segm.gif)

また，あらかじめ処理内容とデータフローがわかっているため，例えばblurを適用しながらエッジ抽出したり，メモリアクセスを効率化するなどして，高速化，メモリ消費量を改善することが可能になります．

## G-APIのアーキテクチャ
G-APIのアーキテクチャを表す図は以下の通りです（[G-API High-level design overview](https://docs.opencv.org/4.0.0/de/d4d/gapi_hld.html)より引用）．

![](https://docs.opencv.org/trunk/gapi_scheme.png)

この図からもわかるようにG-APIのアーキテクチャは以下の3つのレイヤーから構成されています．

- API Layer
  - G-API共通APIによってグラフを実装する
  - G-APIユーザが普段扱うのはこの部分
- Graph Compiler Layer
  - 各種バックエンドの差異を隠蔽する
  - グラフの展開および最適化を行う
- Backends Layer
  - 様々なバックエンド個別の実装（後述）
  - 特定ターゲット向けにチューニングが必要な場合はこのレイヤーを独自実装する必要がある（実装方法は[こちら](https://docs.opencv.org/4.0.0/d0/d25/gapi_kernel_api.html)を参照）

### API Layer
G-APIユーザは，API Layer上で開発することでバックエンドの種類によらずグラフを実装することができるようになっています（上図の「G-API OpenCV Module」がそれに該当）．G-APIでは，グラフの入出力となるデータ型には以下のような専用のデータ構造を用います．

- [cv::GMat](https://docs.opencv.org/4.0.0/df/daa/classcv_1_1GMat.html)
- [cv::GScalar](https://docs.opencv.org/4.0.0/d9/d98/classcv_1_1GScalar.html)
- [cv::GArray](https://docs.opencv.org/4.0.0/d3/d44/classcv_1_1GArray.html)

### Graph Compiler Layer
様々なバックエンドの抽象化およびグラフの展開および最適化を担っているのがGraph Compiler Layerです．G-APIでは[ADE Framework](https://github.com/opencv/ade)にて行われます．**「ADEとかOpenCVのソースの中に入ってたっけ？」**と思われるかもしれませんが，実は[modules/gapi/cmake/DownloadADE.cmake](https://github.com/opencv/opencv/blob/4.0.0/modules/gapi/cmake/DownloadADE.cmake)に以下の記述があり，OpenCVビルド時に[ADE](https://github.com/opencv/ade)が自動ダウンロードされていたりします．

```cmake
ocv_download(FILENAME ${ade_filename}
             HASH ${ade_md5}
             URL
               "${OPENCV_ADE_URL}"
               "$ENV{OPENCV_ADE_URL}"
               "https://github.com/opencv/ade/archive/"
             DESTINATION_DIR ${ade_src_dir}
             ID ADE
             STATUS res
             UNPACK RELATIVE_URL)
```

### Backends Layer
様々なバックエンド個別の実装を担っているのがBackends Layerです．詳細は後述します．

## バックエンドの種類
[Graph-API](https://github.com/opencv/opencv/wiki/Graph-API)では，大別すると以下の3つのバックエンドが紹介されています．

- CPU backend
  - OpenCVのcore，imgprocモジュールをベースとしたCPU実装のバックエンド
  - データフロー，アルゴリズムを検証するときのプロトタイピング用

- GPU backend
  - [OpenCV Transparent API](https://www.learnopencv.com/opencv-transparent-api/)をベースとしたOpenCLバックエンド
  - バックエンド名では「GPU」となっているものの，実態はOpenCLバックエンドであるため，「OpenCL backend」などにリネーム予定らしい

- Fluid backend
  - G-APIのインターフェースに則った独自カーネルを実装したバックエンド

このような作りにすることでユーザはバックエンドによらず共通APIで実装することができ，用途に応じてバックエンドを切り替えることができます（プロトタイプ，アルゴリズム検討はCPU backendでやっておき，速度が要求されるプロダクトではFluid backendを使うなど）．
 
## グラフ構築を事前にやるには
前述の実装ではapplyメソッドの初回実行時にグラフ構築が行われます．そのため，初回実行時はグラフ構築の分だけ処理時間が延びてしまい，用途によっては望ましくない振る舞いかもしれません．

そこで，compileメソッドを使うことで事前にグラフ構築を行うこともできます．このサンプルコードを以下に示します．

```c++
// グラフ定義（処理内容とデータフローをグラフで記述）
cv::GMat in;
cv::GMat gray    = cv::gapi::BGR2Gray(in);
cv::GMat blurred = cv::gapi::blur(gray, cv::Size(5, 5));
cv::GMat out     = cv::gapi::Canny(blurred, 32, 128, 3);
cv::GComputation ac(in, out);

// メタ情報（入力画像サイズ，データ型）の設定
auto meta_info = cv::descr_of(cv::Mat(image.size(), image.type()));

// グラフ構築
auto cc = ac.compile(meta_info);

cv::Mat src, dst;
for (;;)
{
    cap >> src;

    // 事前に構築したグラフを実行
    cc(src, dst);
}
```

ポイントは以下の通りです．

* メタ情報を設定する
* compileメソッドでグラフを構築する
* 実際使うときは事前に構築したグラフを実行

## 注意点
G-APIを使う上での注意点を以下にまとめます．

### ビルドエラー抑制
筆者の開発環境では，G-APIを使ったソースコードをビルドすると以下のようなエラーが出てしまいました（deprecated APIを使っているっぽい）．

<pre>
1>c:\program files (x86)\microsoft visual studio\2017\community\vc\tools\msvc\14.11.25503\include\xutility(2381): error C4996: 'std::copy_n::_Unchecked_iterators::_Deprecate': Call to 'std::copy_n' with parameters that may be unsafe - this call relies on the caller to check that the passed values are correct. To disable this warning, use -D_SCL_SECURE_NO_WARNINGS. See documentation on how to use Visual C++ 'Checked Iterators'
1>c:\program files (x86)\microsoft visual studio\2017\community\vc\tools\msvc\14.11.25503\include\xutility(2381): note: 'std::copy_n::_Unchecked_iterators::_Deprecate' の宣言を確認してください
</pre>

そのため，workaroundとして

```c++
#pragma warning(disable:4996)
```

を付与してビルドしています．

### CPU backendのパフォーマンス
前述の通り，CPU backendは基本的にOpenCVのwrapperとなっています．そのため，G-APIを使ったからといって必ずしも速くなるわけではない点に注意が必要です．

### 後方互換性
G-APIはOpenCV 4.0時点で現在進行系で開発が進められているモジュールです．そのため，[4.0のドキュメント](https://docs.opencv.org/4.0.0/d0/d1e/gapi.html)には

> G-API is a new module and now is in active development. It's API is volatile at the moment and there may be minor but compatibility-breaking changes in the future.

とあり，今後後方互換性が無いAPI変更が行われる可能性がある点に注意が必要です．

## 余談
処理内容とデータフローを連結グラフとして記述するという特徴から[OpenVX](https://www.khronos.org/openvx/)と設計思想が似ているのでは？と思った方もいらっしゃるかもしれません．

（筆者の個人的な感想ですが）OpenVXは仕様が複雑なため取っつきにくかったり，仕様で規定している機能が少ないことから，いきなりOpenVX implementationでプロトタイピングしたり，OpenCVでプロトタイピングしたものをOpenVX implementationにportingするのが一苦労だったりします．G-APIは従来のこのような欠点を克服し，OpenCVを使ってプロトタイプからプロダクトまで同じ枠組みで開発できるようにしているのではないかと考えています．

## おわりに
この記事ではOpenCV 4.0から追加されたG-APIの概要について紹介しました．この記事がG-API理解の助けになれば幸いです．
明日は[roohii_3](https://qiita.com/roohii_3)さんの **「Android（THETA）でOpenCVを動かしてみよう」** という記事です．

## 参考URL
- [Graph-API](https://github.com/opencv/opencv/wiki/Graph-API)
- <https://docs.opencv.org/4.0.0/d0/d1e/gapi.html>
- <https://docs.opencv.org/4.0.0/df/d7e/tutorial_table_of_content_gapi.html>
- <https://github.com/opencv/opencv/blob/4.0.0/modules/gapi/samples/api_example.cpp>
- [OpenCV 4.0のQRコード検出，G-APIを試す ≪ Rest Term](https://rest-term.com/archives/3491/)
- [OpenCV4で追加されたG-APIについて．| ねののお庭．](https://neno-garden.com/blog/2018/11/20/opencv4-g-api/)