search
LoginSignup
10

More than 3 years have passed since last update.

posted at

updated at

openFrameworksでpix2pixを実装する

この記事はYahoo! JAPAN 18 新卒 Advent Calendar 2018 25日目の記事です。
前回は@music431perさんの「Git操作に慣れ始めた人のためのGitTips」でした。

概要

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

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

:mega: サンプルコードはGithubにあります - 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とは


Image-to-Image Translation with Conditional Adversarial Networksより引用

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


論文3の図2より引用

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

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

Tensorflowをビルドする

openFrameworksで実装するにあたり、まずTensorflow5のC++ APIをビルドします:hammer_pick:
Build from source | TensorFlow

Python

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

$ python --version
Python 3.6.5

今回は、3.6.5を使います。
必要なパッケージは、setup.pyREQUIRED_PACKAGESにあります。

Bazel

Tensorflowでは、ビルドツールにBazelを使用します。
今回は、Homebrewを用いてインストールします。

$ 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
Tensorflowのリポジトリをクローンします。300MiBくらいあるので時間がかかります、、、

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

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

$ 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

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

共有ライブラリのコンパイル

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

$ ./configure

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

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

$ 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が見つからないとのエラーが発生しました。

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ライブラリです。
下記のようにインストールすることで解決しました。

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

Tensorflowライブラリをインストール

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

$ 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

アプリケーション開発

いよいよ、openFrameworksを用いて開発していきます。

プロジェクト作成

プロジェクトは、いつも通り作成します。
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にあります。

サンプルを実行

ここまで完了したら、公式にあるサンプルコードを実行します。
C++ API | The basics

#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;
}

実行します。

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という、ラベル画像をもとにストリートの画像を生成してくれるモデルです。

Image-to-Image Translation with Conditional Adversarial Networksより引用

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

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クラスで管理します。
まず、セッションを作成します。

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);
}

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

this->session = create_session();

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

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

セッションを実行する

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

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

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

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への変換も同じようにやっています。

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;
    }
}

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

実行結果

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

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

まとめ

ezgif.com-gif-maker.gif

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

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

最終的なコードはGithubにあるので参照して見て下さい!
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. Tensorflow cppAPI ライブラリを g++ でコンパイル.
    Retrieved from https://toxweblog.toxbe.com/2017/11/10/tensorflow-cppapi-compile-by-g/ 

  5. Tensorflow.
    Retrieved from https://www.tensorflow.org/ 

  6. 高澤佑太, pix2pixの紹介 | 株式会社クロスコンパス's Blog
    Retrieved from https://www.wantedly.com/companies/xcompass/post_articles/76117 

  7. tensorflowをソースからコンパイルしてインストールする/
    Retrieved from http://memo.saitodev.com/home/tensorflow/build/ 

  8. ofxMSATensorFlow.
    Retrieved from https://github.com/memo/ofxMSATensorFlow 

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
What you can do with signing up
10