雰囲気データサイエンティストの備忘録

Atmosphere Data Scientist's Memorandum


3Dメッシュデータをモーフィングする
Python
3Dモデル

概要

PyGeMというモーフィングのライブラリを使って,3Dメッシュデータを動かしてみます。

バージョン情報

python 3.10
pygem 2.0.0
smithers 0.0.1

インストール

追加ライブラリとして,vtk,smithersが必要になります。
他はお好みですが,今回はjupyter上のplotlyで3次元的に可視化しました。

RUN pip install \
    nbformat \
    ipykernel \
    plotly \
    vtk \
    git+https://github.com/mathLab/smithers.git

PyGeMのレポジトリをコピーして,python setup.py installのコマンドを実行するだけでインストールできます。

setup.sh
# pygemがなければgit cloneしてくる
directory="PyGem"
if [ ! -d "$directory" ]; then
    echo "$directory ディレクトリが存在しません。"
    git clone https://github.com/mathLab/PyGeM
fi

# pygemライブラリのセットアップ
cd PyGem
/usr/bin/python3.10 setup.py install

コード

コードを記載していきます。
FFD(Free-Form Deformation)のtutorialをもとに,インプットをobjファイルへ,可視化をplotlyへ修正しています。

インポート

必要な関数をインポートします。
(PyGeMのチュートリアルのファイルから,smithers.io内の構造が変わっているようでした。)

from pygem import FFD
import plotly.graph_objects as go
from smithers.io.obj.objhandler import ObjHandler

ファイルの読み込み

メッシュデータを読み込みます。stanford bunnyのモデルを拾ってきました。

メッシュデータには以下2つの情報が含まれます。

  1. 点の位置情報mesh.vertices
  2. ポリゴンの構成点のインデックス情報(mesh.polygon)

ポリゴン情報は変えずに点の位置だけ変更すれば変形を実現できるため,今回はverticesのみ取り出します。(stanford bunnyの生のファイルはモデルの底面がzx平面となっています。plotlyの可視化のときに都合が悪いので,xy平面が底面になるよう軸を入れ替えます。)

# objファイルを読み込む
objhandler = ObjHandler()
mesh = objhandler.read("bunny/bunny.obj")

# 点群
vertices = mesh.vertices  #WavefrontOBJの変数を参照

# 変換
vertices = vertices[:, [2, 0, 1]]

bounding-boxを配置する

すべての点群が内部に収まる箱(bounding-box)を作ります。

x, y, zそれぞれの方向に点群座標の最大・最小値を取得します。PyGeMのFFDの場合は,各方面の最小値が原点になっており,そこからの長さを定義します。点の位置ぴったりに境界が来ると内外判定が狂うので,ほんの少し箱を大きくしておきます。

# tolerance
tol = 0.00001

# bounding box
min = vertices.min(axis=0)
max = vertices.max(axis=0)
box_origin = min - tol
box_length = abs(min - max) + 2 * tol

# control pointの配置
ffd = FFD([2, 2, 2])
ffd.box_origin = box_origin
ffd.box_length = box_length

変形

制御点番号を指定して変形量を与えます。変形量valはlengthに対しての相対値です。

(実際のところ,数値のみの情報だと変形方向が非常に分かりにくいです。次項の3D描画関数でオブジェクトと制御点の位置関係を確認しながら変形量を決定しました。)

# 変形量
val =  0.5

ffd.array_mu_x[0, 0, 1] = val
ffd.array_mu_y[0, 0, 1] = -val

ffd.array_mu_x[1, 0, 1] = val
ffd.array_mu_y[1, 0, 1] = val

ffd.array_mu_x[0, 1, 1] = -val
ffd.array_mu_y[0, 1, 1] = -val

ffd.array_mu_x[1, 1, 1] = -val
ffd.array_mu_y[1, 1, 1] = val

# FFDに従って点の位置を移動
deformed_vertices = ffd(vertices)

変形前後の確認

plotlyで点群と制御点を可視化します。

def scatter3d(points, control_points, figsize=(1200, 800), s=10, draw=True, alpha=1, labels=["original", "deform"], colors=["gray", "red"]):
    """ 3D可視化用の関数
    """
    fig = go.Figure()

    # 点群の可視化
    for idx, a in enumerate(points):
        trace = go.Scatter3d(
            x=a[:, 0], y=a[:, 1], z=a[:, 2], 
            mode='markers', 
            marker=dict(size=s, opacity=alpha),
            marker_color=colors[idx],
        )
        if labels is not None:
            trace.name = labels[idx]
        fig.add_trace(trace)

    # 制御点の可視化
    for idx, a in enumerate(control_points):
        trace = go.Scatter3d(
            x=a[:, 0], y=a[:, 1], z=a[:, 2],
            mode='markers', 
            marker=dict(size=s*3, opacity=alpha),
            marker_symbol='x',
            marker_color=colors[idx],
        )
        if labels is not None:
            trace.name = labels[idx]
        fig.add_trace(trace)
    
    # 制御点の変化を示す
    for o_pt, d_pt in zip(control_points[0], control_points[1]):
        trace = go.Scatter3d(
                x=[o_pt[0], d_pt[0]],
                y=[o_pt[1], d_pt[1]],
                z=[o_pt[2], d_pt[2]],
                mode='lines', 
                line_color='red',
            )
        fig.add_trace(trace)
    
    
    fig.update_layout(
        scene=dict(xaxis_title='X', yaxis_title='Y', zaxis_title='Z'), 
        width=figsize[0], 
        height=figsize[1],
    )

    if draw:
        fig.show()
    else:
        return fig

# 可視化
scatter3d(
    [vertices, deformed_vertices], 
    s=1, labels=['original', 'deformed'], 
    control_points=[ffd.control_points(deformed=False), ffd.control_points(deformed=True)],
    alpha=0.5,
    draw=False,
)

灰色の点群が変形前,赤色の点群が変形後になります。

bunny_plotly

出力

メッシュをobjファイルに出力します。軸の入れ替えのみ行ったメッシュをorig.objに,変形後のメッシュをdeformed.objに保存します。
元のmeshオブジェクトをコピーしてきてmesh.veticesを上書きします。

import copy

# 元のメッシュを保存する(座標のみ入れ替えている)
orig_mesh = copy.copy(mesh)
orig_mesh.vertices = vertices
objhandler.write(orig_mesh, "bunny/orig.obj")

# 変形後のメッシュをobjファイルに保存
deformed_mesh = copy.copy(mesh)
deformed_mesh.vertices = deformed_vertices

objhandler.write(deformed_mesh, "bunny/deformed.obj")

レンダリングしてみると,モデルがねじり方向に変形してことが分かります。
bunny_render

まとめ

PyGeMでobjファイルの簡単な変形を実現できました。
今回はオブジェクト全体を変形させていますが,耳だけ,首だけといった部分変形ができるともっと自然に動かせそうです。(bounding-boxの配置, 変形量/方向の選定などけっこう大変ですが)

ボリュームメッシュを変形させようとするとまた一段ハードルが上がるのでそのうちトライしてみます。