ディープラーニングフレームワーク Chainer を使って3Dモデルを描画するコードを書きました。
たとえば、コンピュータグラフィックスでよく用いられる teapot.obj を読み込むと、以下のような画像を生成できます。
ソースコードは GitHub で公開しています。どうぞご利用ください。
これは Chainer Advent Calendar の6日目の記事です。
ディープラーニング、流行ってますよね。特に画像認識には強くて、いろいろ面白いことができると話題になっていますよね。
ところで本当は世界って3次元で、我々はそれを2次元に投影したもの(=画像)を見ているわけですよね。この3次元世界を2次元画像へと投影するプロセスもディープラーニングに一気に放り込んでみたいと思いませんか?
そして「3次元世界を2次元画像へと投影するプロセス」に該当するコンピュータ上での処理が3Dモデルの描画、レンダリングであるというわけです。
レンダリングについて Wikipedia の「グラフィクス・パイプライン」の項目に、以下の例が紹介されています。
グラフィックスパイプラインの処理例
今回はこのうち一部の機能だけを実装しました。以下のような関数を通じて、.obj ファイルから画像を生成します。.obj ファイルに含まれる情報のうちテクスチャとマテリアルは無視し、頂点の位置と面の貼り方の情報だけを用います。
意外とシンプルですね。
load_obj は特に何も難しいことはありません。.obj ファイルには「頂点を表す行(3次元空間中の1点)」と「面を表す行(面に対応する頂点の ID の集合)」があるので、それらを読み出してメモリ上に格納します。
look_at は「頂点座標」「どこから見ているか」「どこを見ているか」を受け取り、頂点座標を視点を中心とした座標へと変換します。これは頂点座標に対して回転行列かけ、平行移動ベクトルを足すことで実現できます。床井先生の資料が参考になります。
途中でベクトルの外積の計算が必要になります。これも Chainer で実装しました。chainer.Function の子クラスとして書くと簡単です。
近くのものは大きく、遠くのものは小さく見えます。これを実装したものが perspective です。これは頂点の x, y 座標を z 座標で割り、スケールを調整することで実現できます。これもまた床井先生の資料が参考になります。
vertices_to_faces は頂点の座標の集合(頂点の個数×3D)を面の集合(面の数×面を構成する3点×3D)に変換する補助関数です。
lighting については、環境照明(あらゆる面を照らす照明)と、方位性照明(光源に向かう面ほど明るく見える照明)を実装しました。面上の点の法線ベクトル(面に垂直なベクトル)は頂点ごとに定義される法線ベクトルを補間して求めるのが普通ですが、ここでは手抜きをして、単に面に垂直な方向を法線ベクトルとしています。(なので面がわりとカクカクして見えます。)
ラスタライズ(ピクセル化) rasterize は複雑な処理で、chainer.functions に用意されている関数の組み合わせで書くのは難しいです。そこで chainer.Functions の子クラスを作り CuPy でゴリゴリ実装しました。
高速に処理するために chainer.cuda.elementwise の中にC言語を書きました。これは100行を軽く超える処理で、chainer.cuda.elementwise が想定しているユースケースからは完全に外れていると思いますが、他に良い書き方が思いつかなかったです…。
これらの関数をまとめたクラス Renderer を用意して、以下のように .obj ファイルを描画できるようにしました。簡単ですね!
# load .obj
vertices, faces = neural_renderer.load_obj(args.filename)
vertices = vertices[None, :, :] # [num_vertices, XYZ] -> [batch_size=1, num_vertices, XYZ]
faces = faces[None, :, :] # [num_faces, 3] -> [batch_size=1, num_faces, 3]
# create texture [batch_size=1, num_faces, texture_size, texture_size, texture_size, RGB]
textures = np.ones((1, faces.shape[1], texture_size, texture_size, texture_size, 3), 'float32')
# to gpu
chainer.cuda.get_device_from_id(args.gpu).use()
vertices = chainer.cuda.to_gpu(vertices)
faces = chainer.cuda.to_gpu(faces)
textures = chainer.cuda.to_gpu(textures)
# create renderer
renderer = neural_renderer.Renderer()
# draw object
for num, azimuth in enumerate(range(0, 360, 4)):
renderer.eye = neural_renderer.get_points_from_angles(camera_distance, elevation, azimuth)
images = renderer.render(vertices, faces, textures) # [batch_size, RGB, image_size, image_size]
image = images.data.get()[0].transpose((1, 2, 0)) # [image_size, image_size, RGB]
scipy.misc.imsave('%s/_tmp_%04d.png' % (working_directory, num), image)
加藤大晴と申します。東京大学 大学院情報理工学系研究科 知能機械情報学専攻の博士課程2年です。原田牛久研究室(Machine Intelligence Lab)に所属しています。ソニー株式会社で働いていましたが、学位取得のため休職しています。
Chainer は勾配の計算を自動で行ってくれるため、ほとんどの場合は誤差逆伝播を明示的に書く必要はありません。ただし chainer.Function の子クラスとして実装した Rasterization は誤差逆伝播を自前で記述する必要があります。ところで Rasiterization は厄介で、出力される画像を入力である頂点座標で微分すると答えはゼロか無限大になります。これでは学習がうまくいきません。
Rasterization の微分さえうまく定義できれば、誤差逆伝播も含めてこのレンダラーをディープラーニングに組み込めるわけです。この問題についての論文を執筆し、プロジェクトページを公開しましたので、興味がある方はどうぞご覧ください。
2次元画像上で損失関数を定義し、その勾配を3次元空間へと流し込み、3次元モデルの最適化や3次元空間を介した最適化が行なえます。たとえば、以下のように2次元画像を3次元メッシュにするネットワークを訓練したり、3次元モデルの形状とテクスチャを指定した画風と合うように変換できたりします。
詳しくはプロジェクトページをご覧ください。
そうです。
昨今の人工知能分野では、研究の速報性が重視されるあまり、査読付き学術雑誌/国際会議の発表を待つことなく、ウェブ上に公開された論文を皆が追って読む傾向にあります。こういう状況では、論文の質が査読で担保されないため、有名研究所や有名大学の研究ばかりが注目されることになります。我々のような無名大学(東京大学は世界的には無名です)の学生にとっては不利な状況です。
でも、その状況を黙って見ているのも悔しいですよね。というわけで、有名企業の研究所には全然敵わなくとも少しくらいは宣伝してみようかなと思ったわけです。それもまた博士課程学生の生存戦略のひとつかなと思います。
Chainer で3Dメッシュのレンダラーを書きました。機能はかなり限られていますが、一応
Chainer は自由度の高いフレームワークで、さらには CuPy が CUDA kernel を簡単に書く方法を提供しており、複雑な処理をわりと簡単に書くことができました。これは他のライブラリではなかなか難しいのではないかと思います。Chainer はやはり研究開発に向いたライブラリだということですね。私は Chainer 登場以前は Theano を使っていましたが、これはとても Theano で書こうと思い立つような処理ではないです。
アップロードしたコードはまだバグだらけかと思います。これからも自分の研究に使いながら改修してゆこうと思います。よろしくお願いいたします。
Cpyright (C) 2014-2016 Hiroharu Kato. All Rights Reserved.