openFrameworksでpix2pixを実装する

この記事はYahoo! JAPAN 18 新卒 Advent Calendar 2018 25日目の記事です。
前回は@music431perさんの「[Git操作に慣れ始めた人のためのGitTips](https://qiita.com/music431per/items/49dd209db9393e46318b)」でした。

## 概要
近年メディアアートにおいて、機械学習を用いた事例が多くなってきました。
先日来日していた[Kyle McDonald](https://twitter.com/kcimc)は、機械学習を用いたメディアアートを制作しています。
discrete figures[^1]では、AIや機械学習を用いて未知のダンス表現へ挑戦していました。
AIというメディウムに対して新しい取り組みです。このイベントの制作に関する、素晴らしい記事をカイル自身が書いているので、ぜひ読んで見てください。

<blockquote class="twitter-tweet" data-lang="ja"><p lang="en" dir="ltr">“discrete figures” with <a href="https://twitter.com/rhizomatiks?ref_src=twsrc%5Etfw">@rhizomatiks</a> and <a href="https://twitter.com/elevenplay?ref_src=twsrc%5Etfw">@elevenplay</a>, a new dance performance that uses ML to generate human-like movement <a href="https://t.co/UIE3sYzPIK">https://t.co/UIE3sYzPIK</a></p>&mdash; Kyle McDonald (@kcimc) <a href="https://twitter.com/kcimc/status/1049700162212220928?ref_src=twsrc%5Etfw">2018年10月9日</a></blockquote>


今回は、昨年から注目を集めているpix2pixというアルゴリズムを使ったデモを
openFrameworks[^2]で実装し、機械学習を用いたメディアアートへの一歩を踏み出そうと思います。

:mega: サンプルコードはGithubにあります - [shion1118/ofPix2pix](https://github.com/shion1118/ofPix2pix)

:warning: 本記事では、pix2pixのアルゴリズムやモデルの実装についてではなく、openFrameworksにおけるTensorflowの活用に重点を置いています。

## 開発環境
自分の開発環境は以下の通りです。

```
PC    : MacBook Pro (15-inch, 2017)
CPU   : 2.8 GHz Intel Core i7
Memory: 16 GB 2133 MHz LPDDR3
Xcode : Version 10.1 (10B61)
oF    : 0.10.1 
Python: 3.6.5
Bazel : 0.20.0
GCC   : clang-1000.11.45.5
```

## pix2pixとは
![](https://i.imgur.com/A4Wl0BH.jpg)
*[Image-to-Image Translation with Conditional Adversarial Networks](https://phillipi.github.io/pix2pix/)より引用*

pix2pixは、2016年に「Image-to-Image Translation with Conditional Adversarial Networks[^3]」と題して発表されました。敵対的生成ネットワーク(以下GAN)を用いた画像生成アルゴリズムです。
論文の冒頭にinputとoutputの例があります。画像を入力することで、ペアとなる画像を生成(補間)するものとなっています。


![](https://i.imgur.com/9pFFlZR.png)
*論文[^3]の図2より引用*

入力xからGeneratorが画像G(x)を生成し、Discriminatorは入力xがGeneratorによって生成された画像であるかをTrue/Falseで判別します。Generatorで生成された画像は、偽物であり、元の画像yは本物となります。つまり、GeneratorはDiscriminatorを騙す働きをし、DiscriminatorはGeneratorに騙されないような働きをします。[^5]
通常のGANとは違い、生成後の画像G(x)だけでなく、入力xもペアとして観察し、True/Falseを判別しています。

pix2pixでは、もう一つL1正規化というのも重要なアルゴリズムです。
本記事では省略しますが、ぜひ論文を読んでみて下さい!
公式サイト：[Image-to-Image Translation with Conditional Adversarial Nets](https://phillipi.github.io/pix2pix/)

## Tensorflowをビルドする
openFrameworksで実装するにあたり、まずTensorflow[^6]のC++ APIをビルドします:hammer_pick:
[Build from source | TensorFlow](https://www.tensorflow.org/install/source)

### Python
PythonとTensorflowの依存パッケージをインストールします。

```bash
$ python --version
Python 3.6.5
```
今回は、`3.6.5`を使います。
必要なパッケージは、[setup.py](https://github.com/tensorflow/tensorflow/blob/master/tensorflow/tools/pip_package/setup.py)の`REQUIRED_PACKAGES`にあります。

### Bazel
Tensorflowでは、ビルドツールに[Bazel](https://bazel.build/)を使用します。
今回は、Homebrewを用いてインストールします。

```bash
$ brew tap bazelbuild/tap
$ brew tap-pin bazelbuild/tap
$ brew install bazelbuild/tap/bazeln

$ bazel version
INFO: Invocation ID: 5cd106fb-0375-4904-b4ab-4a4a71772405
Build label: 0.20.0
Build target: bazel-out/darwin-opt/bin/src/main/java/com/google/devtools/build/lib/bazel/BazelServer_deploy.jar
Build time: Fri Nov 30 14:38:25 2018 (1543588705)
Build timestamp: 1543588705
Build timestamp as int: 1543588705
```

### Tensorflowリポジトリをクローン
[tensorflow/tensorflow: An Open Source Machine Learning Framework for Everyone](https://github.com/tensorflow/tensorflow)
Tensorflowのリポジトリをクローンします。300MiBくらいあるので時間がかかります、、、

```bash
$ git clone https://github.com/tensorflow/tensorflow.git
$ cd tensorflow
```

次に利用するバージョンをチェックアウトします。

```bash
$ git tag --sort=version:refname
...
v1.12.0
v1.12.0-rc0
v1.12.0-rc1
v1.12.0-rc2
```
執筆時点(2018/12/20)では、`v1.12.0`が最新です。
公式のリリースからも確認することができます。
[Releases · tensorflow/tensorflow](https://github.com/tensorflow/tensorflow/releases)

```bash
$ git checkout -b v1.12.0 refs/tags/v1.12.0
```

### 共有ライブラリのコンパイル
共有ライブラリ(C++ API)をコンパイルします。
下記コマンドを実行することで、初期設定を行えます。

```bash
$ ./configure
```
途中、y/nで質問がありますが
Macを使っている場合は、Y/nやy/N（大文字がデフォルト）に従えば大丈夫です。
GPUがある場合は、CUDAノバージョンやパスを指定する必要があります。

Bazelを用いてビルドします。

```bash
$ bazel build -c opt //tensorflow:libtensorflow_cc.so

...

Target //tensorflow:libtensorflow_cc.so up-to-date:
  bazel-bin/tensorflow/libtensorflow_cc.so
INFO: Elapsed time: 2231.907s, Critical Path: 214.42s
INFO: 4193 processes: 4191 darwin-sandbox, 2 local.
INFO: Build completed successfully, 4257 total actions

$ ls -lh bazel-bin/tensorflow/libtensorflow_cc.so
-r-xr-xr-x  1 shfukush  wheel   177M 12 13 15:08 bazel-bin/tensorflow/libtensorflow_cc.so
```
環境依存ですが自分の場合は12分ほどかかりました。
ビルドが成功すると、`bazel-bin/tensorflow/`配下に共有ライブラリがビルドされます。

#### fatal error: 'ares.h' file not found
初回に`aresh.h`が見つからないとのエラーが発生しました。

```bash 
external/grpc/src/core/ext/filters/client_channel/resolver/dns/c_ares/grpc_ares_ev_driver_posix.cc:23:10: fatal error: 'ares.h' file not found
#include <ares.h>
         ^~~~~~~~
1 error generated.
```
aresは、非同期DNSリクエストに関するCライブラリです。
下記のようにインストールすることで解決しました。

```bash
$ git clone https://github.com/c-ares/c-ares.git
$ cd c-ares
$ ./configure
$ make
$ make install
```

### Tensorflowライブラリをインストール
ビルドされた共有ライブラリ`libtensorflow_cc.so`をインストールします[^4]。
Macでは、`.so`を`.dylib`にする必要があるので注意してください。

```bash
$ install -m 0644 bazel-bin/tensorflow/libtensorflow_cc.so /usr/local/lib/libtensorflow_cc.dylib
$ install_name_tool -id /usr/local/lib/libtensorflow_cc.dylib /usr/local/lib/libtensorflow_cc.dylib
```

次にヘッダーファイルをコピーします。（今回は`/usr/local/include/tensorflow/`）
Tensorflow用いたC++ソースをコンパイルする際は、このディレクトリをインクルードパスに指定します[^7]。
下記リンクのスクリプトを実行することでコピーできます。

[Copy tensorflow headers](https://gist.github.com/shion1118/03c2ad587b1b83a6adfc48c8976b440e)

## アプリケーション開発
いよいよ、openFrameworksを用いて開発していきます。

### プロジェクト作成
プロジェクトは、いつも通り作成します。
[Create a new project](https://openframeworks.cc/learning/01_basics/create_a_new_project/)

作成後にプロジェクトに対してTensorflowのパスなどを設定します。
設定項目は下記の3つです。

|項目  |パス |
|---|---|
|Header Search Paths |/usr/local/include/tensorflow/ |
|Library Search Paths |/usr/local/lib/ |
|Link Binary With Libraries |libtensorflow_cc.dylib  |

Link Binary With Librariesの設定だけは、`Build Phases`にあります。
![](https://i.imgur.com/qU1FfJ3.png)

### サンプルを実行
ここまで完了したら、公式にあるサンプルコードを実行します。
[C++ API | The basics](https://www.tensorflow.org/guide/extend/cc#the_basics)

```cpp
#include "ofMain.h"
#include "ofApp.h"
#include "tensorflow/cc/client/client_session.h"
#include "tensorflow/cc/ops/standard_ops.h"
#include "tensorflow/core/framework/tensor.h"

int main() {
  using namespace tensorflow;
  using namespace tensorflow::ops;
  Scope root = Scope::NewRootScope();
  // Matrix A = [3 2; -1 0]
  auto A = Const(root, { {3.f, 2.f}, {-1.f, 0.f} });
  // Vector b = [3 5]
  auto b = Const(root, { {3.f, 5.f} });
  // v = Ab^T
  auto v = MatMul(root.WithOpName("v"), A, b, MatMul::TransposeB(true));
  std::vector<Tensor> outputs;
  ClientSession session(root);
  // Run and fetch v
  TF_CHECK_OK(session.Run({v}, &outputs));
  // Expect outputs[0] == [19; -3]
  LOG(INFO) << outputs[0].matrix<float>();
  return 0;
}
```

実行します。

```bash
2018-12-17 19:09:01.822704: I tensorflow/core/platform/cpu_feature_guard.cc:141] Your CPU supports instructions that this TensorFlow binary was not compiled to use: SSE4.2 AVX AVX2 FMA
2018-12-17 19:09:01.839162: I .../apps/myApps/pix2pix/src/main.cpp:27] 19
-3
```
このように表示されれば、成功です。
これでTensorflowを自由に使うことができます！

### モデル(グラフ)を読み込む
本来は、モデルを実装・学習しますが、今回はすでに学習済みのモデルを使用します。
使用するのはLabels to Street Sceneという、ラベル画像をもとにストリートの画像を生成してくれるモデルです。
![](https://i.imgur.com/qsZlNGB.png)
*[Image-to-Image Translation with Conditional Adversarial Networks](https://phillipi.github.io/pix2pix/)より引用*

ReleaseのAssetsにある`labels_2_street_scene.zip
`をダウンロードして下さい。
[Release v1.0.0 · shion1118/oF-pix2pix](https://github.com/shion1118/oF-pix2pix/releases/tag/v1.0.0)
その後、`graph_l2s.pb`をプロジェクトフォルダの`bin/data/`に配置します。
モデルは、GraphDefクラスで扱います。`tensorflow::ReadBinaryProto`にモデルのファイルパスとモデル(グラフ)のポインタを渡すことで読み込めます。

```cpp
shared_ptr<tensorflow::GraphDef> Model::load_graph() {
    shared_ptr<tensorflow::GraphDef> graph(new tensorflow::GraphDef());
    ofLog(OF_LOG_NOTICE, "Load graph: " + this->model_path);
    tensorflow::Status status = tensorflow::ReadBinaryProto(tensorflow::Env::Default(), this->model_path, graph.get());
    if (!status.ok()) {
        ofLog(OF_LOG_ERROR, "Cannot load graph!!");
        return nullptr;
    }
    return graph;
}
```

### セッションを作成する
Tensorflowでは、モデルや推論の実行などを`Session`クラスで管理します。
まず、セッションを作成します。

```cpp
shared_ptr<tensorflow::Session> Model::create_session() {
    tensorflow::Session* session;
    ofLog(OF_LOG_NOTICE, "Create session.");
    tensorflow::Status status = tensorflow::NewSession(tensorflow::SessionOptions(), &session);
    if (!status.ok()) {
        ofLog(OF_LOG_ERROR, "Cannot create session!!");
        return nullptr;
    }
    return shared_ptr<tensorflow::Session>(session);
}
```

作成した後に、先ほどのモデルを設定します。

```cpp
this->session = create_session();
    
tensorflow::Status status = this->session->Create(*graph);
if (!status.ok()) {
    ofLog(OF_LOG_ERROR, status.ToString());
    return;
}
```

セッションが作成できれば、実際に実行していきましょう！

### セッションを実行する
実行は非常にシンプルです。
引数で必要なのは、入力テンソル・出力のオペレーション名・出力テンソルです。
実際にコードを見ていただければわかりますが、今回のモデルの出力オペレーションは`"generator/generator_outputs"`となっています。

```cpp
session->Run(input_tensors, output_op_names, {}, &output_tensors);
```

少し考えなければいけないのが、`ofImage`を`Tensor`に(逆)変換することです。
今回は、このように実装しました。（ご指摘などはPRやIssueでお願いします)
`ofImage`を参照しピクセル情報を、あらかじめ初期化しておいた`Tensor`に直接代入しています。
画像データなどは重いので参照渡しする方がいいように思います。

```cpp
void Utils::image_to_tensor(const ofFloatImage &src, tensorflow::Tensor &dst, const ofVec2f &in_range, const ofVec2f &out_range) {
    auto *in = src.getPixels().getData();
    auto dst_data = dst.flat<float>().data();
    int n = dst.NumElements();
    float scale, offset;
    
    calc_scale_offset(in_range, out_range, scale, offset);
    for (int i = 0; i < n; i++) {
        dst_data[i] = in[i] * scale + offset;
    }
}
```

`Tensor`から`ofImage`への変換も同じようにやっています。

```cpp
void Utils::tensor_to_image(const tensorflow::Tensor &src, ofFloatImage &dst, const ofVec2f& in_range, const ofVec2f& out_range) {
    auto *out = dst.getPixels().getData();
    auto src_data = src.flat<float>().data();
    int n = src.NumElements();
    float scale, offset;
    
    calc_scale_offset(in_range, out_range, scale, offset);
    for (int i = 0; i < n; i++) {
        out[i] = src_data[i] * scale + offset;
    }
}
```

スケールとオフセットの計算は、ofxMSATensorflow[^8]を参考に実装しました。

### 実行結果
同じくAssetsに256x256のテスト画像`test_image.jpg`があるので、
これを`ofImage`で読み込めば、テスト実行できるかと思います。


![](https://i.imgur.com/OyobV4a.png)

Mac(CPU)の環境で、1秒前後で実行することができました！
GPUがあれば、リアルタイム処理もできると思います。

## まとめ
![ezgif.com-gif-maker.gif](https://qiita-image-store.s3.amazonaws.com/0/35859/8ff2013a-bb75-1ffb-5640-d0c2f86566cc.gif)

FBOを用いて自分で書いた画像から生成できるようにしました。
カラーパレットから色を選択して描いた画像から実行することができます。

機械学習を用いたメディアアートへの一歩を踏み出せたんじゃないでしょうか？
先日参加したBRDGにおいて[Keijiro Takahashi](https://www.keijiro.tokyo/)は、pix2pixをフレーム予測に応用しVJを試みていました。
[深層学習を勉強してリアルタイム映像表現に使ってみた話](https://qiita.com/keijiro/items/8874c5730feaf80b7943)
今回のやっているのは、あくまでpix2pixをopenFrameworksで実行できるようになっただけです。日々様々なところで使われる「AI」という言葉、驚くべき速さで進歩する深層学習、ぜひこのメディウムを問い直して欲しいと思います。このメディウムをどう扱い、どのように表現するか、その手段は意外にも簡単に手に入ります。

最終的なコードはGithubにあるので参照して見て下さい！
[shion1118/ofPix2pix](https://github.com/shion1118/ofPix2pix)
ご指摘などはPRやIssueでお願いします:bow:

最後まで読んでいただき、ありがとうございました。

[^1]: *discrete figures -ELEVENPLAY×Rhizomatiks Research×Kyle McDonald*. 
      Retrieved from https://research.rhizomatiks.com/s/works/discrete_figures/
[^2]: *openFrameworks*.
      Retrieved from https://openframeworks.cc/
[^3]: Phillip Isola, Jun-Yan Zhu et al., *Image-to-Image Translation with Conditional Adversarial Networks*.
      Retrieved from https://arxiv.org/abs/1611.07004
[^4]: 高澤佑太, *pix2pixの紹介 | 株式会社クロスコンパス's Blog*
      Retrieved from https://www.wantedly.com/companies/xcompass/post_articles/76117
[^5]: *Tensorflow cppAPI ライブラリを g++ でコンパイル*.
      Retrieved from https://toxweblog.toxbe.com/2017/11/10/tensorflow-cppapi-compile-by-g/
[^6]: *Tensorflow*.
      Retrieved from https://www.tensorflow.org/
[^7]: *tensorflowをソースからコンパイルしてインストールする*/
      Retrieved from http://memo.saitodev.com/home/tensorflow/build/
[^8]: *ofxMSATensorFlow*.
      Retrieved from https://github.com/memo/ofxMSATensorFlow