NeRF検討用データセットをBlender Pythonスクリプトで作成する方法

# はじめに
はじめまして、Live2D社AIチームリーダーの澤田です。
この度、Live2D社ではQiitaでの技術情報の発信を始めることにいたしました。
QiitaではLive2Dという枠組みに囚われずに様々な技術トピックを扱っていこうと思っています。

今回はDeepLearningのCG応用として現在盛んに研究されているNeRFと呼ばれる技術の検討用データセットを、BlenderのPythonスクリプトで作成する方法について紹介いたします。
NeRFに限らず、様々なデータセットの作成に応用可能なので、画像系の機械学習に興味のある方全般にとって役に立つ情報だと思います。

# NeRFとは
NeRF（Neural Radiance Fields）とは、[NeRF: Representing Scenes as Neural Radiance Fields for View Synthesis](https://arxiv.org/abs/2003.08934)で提案された技術であり、被写体を複数の視点から撮影した画像を学習することで、その被写体を任意の視点から撮影した画像の生成を可能にします（下の画像は論文から引用）。
![nerf.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2498951/010dc50e-59cc-78a9-418b-920099e0c283.png)

# Blender Pythonスクリプトとは
[Blender](https://blender.jp/)はオープンソースの統合3DCGソフトウェアです。
BlenderではPythonスクリプトを使って様々な処理を自動化できます。
本記事では、「カメラを動かしながらレンダリングを行い、レンダリング画像を保存していく」作業をPythonスクリプトで自動化します。
以下では主にPythonスクリプトの中身について解説していきますので、Blender上でのPythonスクリプト自体の使い方については、[こちらのページ](https://www.kkaneko.jp/db/cg/bpy.html)などを参照してください。

# Blenderファイル
被写体として、NeRF論文の著者が公開しているBlenderファイルを利用します。[（リンク）](https://drive.google.com/drive/folders/128yBriW1IG_3NJ5Rp7APSTZsJqdJdfc1)
上記リンク先からblend_files.zipをダウンロードし、"lego.blend"というファイルをBlenderで読み込んでください。
上記リンク先のファイル群は[Ben Mildenhall](https://github.com/bmild)によって[CC BY 3.0](https://creativecommons.org/licenses/by/3.0/deed.ja)の下に公開されています。本記事で紹介するPythonスクリプトは、その二次創作物となっております。
![lego.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2498951/e1c466fa-5a6a-6010-d904-a3833c41f414.png)


# Pythonスクリプト(全体)
BlenderのScriptingタブから、logo.blend付属の"360_view.py"と"360_view_test.py"を開くことができます。
こちらをそのまま利用してもよいのですが、撮影画像だけでなくデプスマップを同時に出力するなど複雑な記述になっているため、本記事では最低限必要なスクリプトのみを抽出・編集し、その中身を解説します。
以下がそのPytnonスクリプトです。
```python
import bpy
import os
import numpy as np
import json

VIEWS = 100
RESOLUTION = 128
RESULTS_PATH = os.path.expanduser('~/Desktop/nerf_blender_qiita')
if not os.path.exists(RESULTS_PATH):
    os.makedirs(os.path.join(RESULTS_PATH, 'train'))

# Render Setting
scene = bpy.context.scene
scene.render.resolution_x = RESOLUTION
scene.render.resolution_y = RESOLUTION
scene.render.image_settings.file_format = str('PNG')
scene.render.film_transparent = True
scene.render.use_persistent_data = True

# Camera Setting
cam = scene.objects['Camera']
cam.location = (0, 4.0, 0.0)

# Camera Parent
camera_parent = bpy.data.objects.new("CameraParent", None)
camera_parent.location = (0, 0, 0)
scene.collection.objects.link(camera_parent)
cam.parent = camera_parent

# Camera Constraint
cam_constraint = cam.constraints.new(type='TRACK_TO')
cam_constraint.track_axis = 'TRACK_NEGATIVE_Z'
cam_constraint.up_axis = 'UP_Y'
cam_constraint.target = camera_parent

# Data to store in JSON file
out_data = {'camera_angle_x': bpy.data.objects['Camera'].data.angle_x, 'frames': []}

for i in range(VIEWS):

    # Random Camera Rotation
    camera_parent.rotation_euler = np.array([np.random.uniform(np.pi / 12, np.pi / 2), 0, np.random.uniform(0, 2 * np.pi)])

    # Rendering
    filename = 'r_{0:03d}'.format(i)
    scene.render.filepath = os.path.join(RESULTS_PATH, 'train', filename)
    bpy.ops.render.render(write_still=True)

    # add frame data to JSON file
    def listify_matrix(matrix):
        matrix_list = []
        for row in matrix:
            matrix_list.append(list(row))
        return matrix_list
    frame_data = {
        'file_path': './train/' + filename,
        'transform_matrix': listify_matrix(cam.matrix_world)
    }
    out_data['frames'].append(frame_data)

with open(os.path.join(RESULTS_PATH, 'transforms_train.json'), 'w') as out_file:
    json.dump(out_data, out_file, indent=4)
```

# Pythonスクリプト（解説）
スクリプトの中身について一つ一つ解説していきます。

### パラメータと生成データ保存先の設定
```python
VIEWS = 100
RESOLUTION = 128
RESULTS_PATH = os.path.expanduser('~/Desktop/nerf_blender_qiita')
if not os.path.exists(RESULTS_PATH):
    os.makedirs(os.path.join(RESULTS_PATH, 'train'))
```
VIEWSは視点数（生成画像数）、RESOLUTIONは生成画像の解像度、RESULTS_PATHは生成データの保存先です。

### レンダリング設定
```python
# Render Setting
scene = bpy.context.scene
scene.render.resolution_x = RESOLUTION
scene.render.resolution_y = RESOLUTION
scene.render.image_settings.file_format = str('PNG')
scene.render.film_transparent = True
scene.render.use_persistent_data = True
```
resolution_xとresolution_yは生成画像の解像度、file_formatは生成画像ファイルのフォーマット（拡張子）、film_transparentは背景透過の有無、use_persistent_dataはレンダリングデータを保存して再レンダリングを高速化するか否かの設定です。

### カメラ設定
```python
### Camera Setting
cam = scene.objects['Camera']
cam.location = (0, 4.0, 0.0)
```
カメラ初期位置の設定です。
（原点からの距離は4.0で、この設定が次のcamera_parentに関連してきます）

### 空オブジェクトを生成し、カメラの親に設定
```python
# Camera Parent
camera_parent = bpy.data.objects.new("CameraParent", None)
camera_parent.location = (0, 0, 0)
scene.collection.objects.link(camera_parent)
cam.parent = camera_parent
```
原点(0, 0, 0)にcamera_parentという空オブジェクトを生成し、それをカメラの親に設定しています。
このように設定するとcamera_parentを回転させるだけで、cameraを半径4.0の半球上で移動させることができます。
逆にこのような設定を行わない場合、カメラの姿勢を変換する度にカメラの位置を算出し直す必要があります（下画像参照）。
<img width="400" alt="fig1-2-1.gif" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2498951/864d0573-82ab-80fd-ccde-0fb9cb0ba659.gif">

引用：http://fnorio.com/0098spherical_trigonometry1/spherical_trigonometry1.html

### カメラコンストレイント
```python
# Camera Constraint
cam_constraint = cam.constraints.new(type='TRACK_TO')
cam_constraint.track_axis = 'TRACK_NEGATIVE_Z'
cam_constraint.up_axis = 'UP_Y'
cam_constraint.target = camera_parent
```
上記のcamera_parentの設定だけだと、camera_parentを回転させた場合に、カメラの位置は半球上を移動するものの、カメラの視線方向が原点にある被写体から外れてしまいます。
そこで、カメラコンストレイントによって、カメラが常に原点（camera_parent）の方を見るように設定しています。

### JSONファイルに出力する情報の準備
```python
# Data to store in JSON file
out_data = {'camera_angle_x': bpy.data.objects['Camera'].data.angle_x, 'frames': []}
```
このスクリプトでは画像だけでなく、NeRFの学習に必要なカメラの画角や姿勢の情報をJSONファイルとして出力します。
それらの情報を格納するためのdictを初期化しています。

### カメラの姿勢設定とレンダリング
```python
for i in range(VIEWS):

    # Random Camera Rotation
    camera_parent.rotation_euler = np.array([np.random.uniform(np.pi / 12, np.pi / 2), 0, np.random.uniform(0, 2 * np.pi)])

    # Rendering
    filename = 'r_{0:03d}'.format(i)
    scene.render.filepath = os.path.join(RESULTS_PATH, 'train', filename)
    bpy.ops.render.render(write_still=True)
```
VIEWS回ループを回します。
各ループでは、カメラの姿勢をランダムに決定し、そのカメラ姿勢でレンダリングした結果を画像ファイルに出力します。

### JSONファイル出力用の情報を追加
```python
# add frame data to JSON file
def listify_matrix(matrix):
    matrix_list = []
    for row in matrix:
        matrix_list.append(list(row))
    return matrix_list
frame_data = {
    'file_path': './train/' + filename,
    'transform_matrix': listify_matrix(cam.matrix_world)
}
out_data['frames'].append(frame_data)
```
JSONファイル出力用のdictに「画像ファイルのファイル名(file_path)」と「カメラ姿勢(transform_matrix)」を追加します。

### JSONファイルの出力
```python
with open(os.path.join(RESULTS_PATH, 'transforms_train.json'), 'w') as out_file:
    json.dump(out_data, out_file, indent=4)
```
JSONファイルを出力します。

# スクリプトの生成物
上記のスクリプトを実行すると、レンダリング画像群とJSONファイルが出力されます。
![imgs.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2498951/2a4c5150-ec4a-5646-0671-5693f60c6308.png)


# まとめ
以上、NeRF検討用のデータセットをBlender Pythonスクリプトで作成する方法を紹介しました。
スクリプトの中身を理解することで、様々な研究のデータセット作成に応用していただけるかと思います。

# Live2D社でのAI研究について
Live2D社では、[AI研究ポリシー](https://docs.nizima.com/ai-research-policy/)に従って、AI研究を進めています。
ご興味のあるエンジニアの方は[リクルートページ](https://www.live2d.jp/recruit/)をご覧ください。