かみのメモ

コンピュータビジョン・プログラムな話題中心の勉強メモ

Blenderをちょっとした座標系可視化ツールとして使う

小ネタです。

今回、ちょっと複雑なプロジェクタ-カメラ系のキャリブレーションをすることになり、「さすがにキャリブレーション結果を可視化して確認したいな」と思ってスクリプトを書いてみました。

今回書いたのは、世界座標系に対する回転行列と並進ベクトルが与えられたとき、ローカル座標系をBlender上で可視化するPythonスクリプトです。

f:id:kamino-dev:20200925172641p:plain
ローカル座標系をBlenderで可視化した様子

スポンサーリンク

もくじ

0. 実行環境

macOS Mojave 10.14.6
Blender 2.82
Blender Python 3.7.4

1. なぜBlender

matplotlib, Plotly, WebGLなどの可視化ツールではなくBlenderを使ったのは、以下の理由からです。

  1. 無料
  2. インストールが楽
  3. デフォルトでグリッドが描画されていて、マウスでぐるぐる回す操作も直感的

特に最後の理由が大きいですね。 3Dを描画できるツールはたくさんあるのですが、視点移動やズームのUI設計が雑なものが意外と多いです。 その点、Blenderのマウス操作はかなり直感的で優秀だと思います。

2. 書いたスクリプト

Blenderは、すべてのオペレーションに対してPython APIを用意し、それをbpyモジュールとして提供してくれています(そもそもBlender UIがPythonを通して実装されているらしい?)。

冒頭に書いたように、今回は世界座標系からローカル座標系までの回転行列並進ベクトルが与えられたとき、ローカル座標系のXYZ軸を3本の矢印で描画する関数draw_coord_systemを書きました。

import bpy
import numpy as np
from scipy.spatial.transform import Rotation


def draw_coord_system(name, rot, tvec):
    # coneとcylinderで矢印を描画する
    bpy.ops.mesh.primitive_cone_add(
        radius1=0.2, depth=0.4, location=(0, 0, 2.2))
    bpy.ops.mesh.primitive_cylinder_add(
        radius=0.1, depth=2, location=(0, 0, 1))

    cone = bpy.data.objects['Cone']
    cylinder = bpy.data.objects['Cylinder']

    # 2つをjoinして1つのオブジェクトにする
    ctx = bpy.context.copy()
    ctx['active_object'] = cone
    ctx['selected_objects'] = cylinder
    ctx['selected_editable_objects'] = [cone, cylinder]
    bpy.ops.object.join(ctx)

    arrow = cone
    arrow.name = 'axis_arrow'

    # オブジェクトの原点を矢印の根本に設定する
    arrow.select_set(True)
    bpy.context.scene.cursor.location = (0, 0, 0)
    bpy.ops.object.origin_set(type='ORIGIN_CURSOR')

    # 赤青緑の単色マテリアルを定義する
    x_mat = bpy.data.materials.new("x_mat")
    x_mat.diffuse_color = (1, 0, 0, 1)
    y_mat = bpy.data.materials.new("y_mat")
    y_mat.diffuse_color = (0, 1, 0, 1)
    z_mat = bpy.data.materials.new("z_mat")
    z_mat.diffuse_color = (0, 0, 1, 1)

    # 最初に作った矢印をZ軸とする
    me = arrow.data
    z_axis = arrow
    z_axis.name = 'z_axis'

    # Z軸をコピーし、オブジェクト原点の周りで回転させてX軸・Y軸を作る
    y_axis = bpy.data.objects.new('y_axis', me.copy())
    bpy.context.scene.collection.children[0].objects.link(y_axis)
    y_axis.rotation_euler[0] = -np.pi/2

    x_axis = bpy.data.objects.new('x_axis', me.copy())
    bpy.context.scene.collection.children[0].objects.link(x_axis)
    x_axis.rotation_euler[1] = np.pi/2

    # それぞれにマテリアルを適用する
    z_axis.data.materials.append(z_mat)
    y_axis.data.materials.append(y_mat)
    x_axis.data.materials.append(x_mat)

    # 3軸を1つのオブジェクトにまとめる
    ctx = bpy.context.copy()
    ctx['active_object'] = z_axis
    ctx['selected_objects'] = [x_axis, y_axis, z_axis]
    ctx['selected_editable_objects'] = [x_axis, y_axis, z_axis]
    bpy.ops.object.join(ctx)

    axis = z_axis
    axis.name = name

    # 指定されたローカル座標系の場所に移動させる
    axis.rotation_mode = 'XYZ'
    axis.rotation_euler = rot.as_euler('xyz')
    axis.select_set(True)
    axis.location = tvec

細かいことはBlenderのドキュメントやStack Overflowを参照していただきたいのですが、構造としてはmesh, material, objectのテーブルにレコードを挿入し、できたobjectレコードをscene.collectionに挿入するとUIに描画されるようになっています。

今回、複数の座標系を描画する際に同じmeshデータやmaterialデータを使い回すように実装しても良かったのですが、後から個別に形状や色を変えるかもしれないので結局は毎回新しいものを作成するようにしました。

また、オブジェクトを回転させる処理にはクォータニオンを使おうかとも思ったのですが、そのためにはaxis.rotation_mode = 'quaternion'のように設定を変更しなければなりません。 BlenderはデフォルトではXYZのintrinsicオイラー角を使っているようなので、今回はその流儀に従ってオイラー角を使う実装にしました。

この関数を以下のように呼び出せば、ローカル座標系がBlenderのシーンの中に描画されます。

rot = Rotation.from_euler('XYX', [90, 90, 10], degrees=True)
tvec = np.array([1, 1, 1])
draw_coord_system('camera_system', rot, tvec)

3. scipyのインストール

今回のスクリプトでは、三次元回転を扱うためにscipyのRotationモジュールを利用しています。

※ このモジュールの詳細は以前の記事で紹介しています。

ということで、実行前にscipyをインストールしておきます。

Blenderは我々が手動でインストールしたPythonではなく、Blenderインストール時に内包されているPythonを使います。 scipyをインストールするときは、そのPythonバイナリを通してpipを呼び出すようにしましょう。

例えば、私のMac環境では、以下のコマンドでscipyをインストールできました。

/Applications/Blender.app/Contents/Resources/2.82/python/bin/python3.7m -m pip install scipy

4. スクリプトの実行

一連の処理を書いたPythonファイルを適当な場所に保存したら、Blenderを開きます。 デフォルトのcubeは邪魔なので消しておきましょう。

Blenderが起動したら、レイアウトをScriptingに変更します。 すると画面右にスクリプトエディタが開くので、上部のボタンを押して保存しておいたPythonファイルを開きましょう。

ファイルを開いた後、Run Scriptのボタンを押せばスクリプトが実行され、左上の画面に作成したオブジェクトが反映されます。


以上、Blenderで座標系を可視化するという小ネタでした。

ちなみに、このスクリプトを書くきっかけになったプロジェクトですが、オブジェクトトラッキングの方法を変えることになったので今回書いたスクリプトが活躍することはありませんでした…。

まあ、コードを取っておけば、また使う機会も来るでしょう。