TadaoYamaokaの開発日記

個人開発しているスマホアプリや将棋AIの開発ネタを中心に書いていきます。

Free-Form Deformationでテクスチャを変形する

前回までの日記で、OpenGLでテクスチャ(2D画像)を平面に描画する方法と、ベジエ曲面を描画する方法について記述した。
この2つを組み合わせて、テクスチャの変形を行う。

テクスチャの変形には、自由形状変形(FFD: Free-Form Deformation)という手法を使う。
FFDは、ベジエ曲面を用いて座標系を変形する。

FFDによる座標系(u, v)の変形は、制御点を\mathbf{P_{ij}}とすると、以下の式で表される。
\displaystyle
\begin{eqnarray*}
\mathbf{X}(u, v) &=& \sum_{i=0}^{m} \sum_{j=0}^{n} B_i^m(u) B_j^n(v) \mathbf{P_{ij}},\;\;\; 0 \leq u \leq 1, 0 \leq v \leq 1 \\
B_i^n(t) &=& {}_nC_i t^i(1 - t)^{n-i}
\end{eqnarray*} \tag{1}
これは、前回の日記で説明したベジエ曲面の式と同じである。

FFDを使用してテクスチャを変形するには、テクスチャを格子状の頂点で構成される面に貼り付けた後、頂点をFFDで変形する。
なお、テクスチャを貼り付ける面は格子状でなくてもよい。この記事では簡単なため格子状とした。

Live2Dの曲面デフォーマーは、このFFDを使用していると思われる。
これによりLive2Dと同じような変形が一部実現できる。

Pythonで記述すると以下のようになる。

import sys
from OpenGL.GL import *
from OpenGL.GLU import *
from OpenGL.GLUT import *
from PIL import Image
import numpy as np
import scipy.misc as scm

def bernstein(n, i, t):
    return scm.comb(n, i) * t**i * (1 - t)**(n-i)

def bezier_patches(m, n, u, v, q):
    return np.dot([bernstein(n, j, v) for j in range(n + 1)], np.tensordot([bernstein(m, i, u) for i in range(m + 1)], q, axes=1))

def load_texture():
    img = Image.open("sample2.png")
    w, h = img.size
    glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, w, h, 0, GL_RGBA, GL_UNSIGNED_BYTE, img.tobytes())

def display():
    #P = np.array([
    #    [[ 0. , 0. ], [ 0. , 1/3], [ 0. , 2/3], [ 0. , 1. ]],
    #    [[ 1/3, 0. ], [ 1/3, 1/3], [ 1/3, 2/3], [ 1/3, 1. ]],
    #    [[ 2/3, 0. ], [ 2/3, 1/3], [ 2/3, 2/3], [ 2/3, 1. ]],
    #    [[ 1. , 0. ], [ 1. , 1/3], [ 1. , 2/3], [ 1. , 1. ]],
    #    ])
    P = np.array([
        [[ 0. , 0. ], [ 0. , 0.3], [ 0. , 0.6], [ 0. , 1. ]],
        [[ 0.3, 0.1], [ 0.3, 0.4], [ 0.3, 0.8], [ 0.3, 1.1]],
        [[ 0.6, 0.1], [ 0.7, 0.4], [ 0.7, 0.7], [ 0.6, 1. ]],
        [[ 1. , 0. ], [ 1.1, 0.3], [ 1.1, 0.6], [ 1. , 1. ]],
        ])

    X = np.zeros((11 * 11, 2))
    T = np.zeros((11 * 11, 2)) # texture
    for i in range(11):
        for j in range(11):
            u = i / 10.0
            v = j / 10.0
            X[i * 11 + j] = bezier_patches(3, 3, u, v, P) # FFD

            T[i * 11 + j] = [i / 10.0, 1.0 - j / 10.0]

    V = np.zeros((11 * 11, 3))
    for i in range(11):
        for j in range(11):
            V[i * 11 + j] = [X[i * 11 + j, 0] - 0.5, X[i * 11 + j, 1] - 0.5, 0]

    IDX = np.zeros((10, 11 * 2))
    for i in range(10):
        for j in range(11):
            IDX[i, j * 2] = i * 11 + j
            IDX[i, j * 2 + 1] = (i + 1) * 11 + j

    glClear(GL_COLOR_BUFFER_BIT)

    glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA)
    glEnable(GL_BLEND)

    glEnableClientState(GL_VERTEX_ARRAY)
    glEnableClientState(GL_TEXTURE_COORD_ARRAY)

    glEnable(GL_TEXTURE_2D)

    glVertexPointerf(V)
    glTexCoordPointerf(T)

    for idx in IDX:
        glDrawElementsui(GL_TRIANGLE_STRIP, idx)

    glDisable(GL_TEXTURE_2D)

    glDisable(GL_BLEND)

    glColor3f(1.0, 1.0, 1.0)
    glPointSize(2.0)
    glBegin(GL_POINTS)
    for p in P.reshape((-1, 2)):
        glVertex2f(p[0] - 0.5, p[1] - 0.5)
    glEnd()

    glFlush()

def main():
    glutInit(sys.argv)
    glutInitDisplayMode(GLUT_RGBA)
    glutInitWindowSize(300, 300)
    glutCreateWindow(b"FFDSample1")
    glutDisplayFunc(display)
    glClearColor(0.0, 0.0, 1.0, 1.0)

    load_texture()
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR)
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR)

    glutMainLoop()

if __name__ == '__main__':
    main()

1.0×1.0の大きさの面を10分割してテクスチャを貼り付けている。
4×4の制御点を等間隔の格子から少しずらしている。
その制御点を使用して、FFDで、テクスチャを貼り付けた頂点を変形している。
テクスチャを貼り付けた面の中心が(0, 0)になるように(-0.5, -0.5)だけ移動して表示している。

実行結果
  • 変形前

f:id:TadaoYamaoka:20170309225604p:plain

  • 変形後

f:id:TadaoYamaoka:20170309225659p:plain

白い点は制御点を示している。

参考文献

Pythonでベジエ曲面を描く

前回の日記Pythonベジエ曲線を描いたが、今回はベジエ曲面(Bezier patches)を描く。

ベジエ曲面は、以下の式で表される。
\displaystyle
P(u, v) = \sum_{i=0}^{m} \sum_{j=0}^{n} B_i^m(u) B_j^n(v) \mathbf{q_{ij}},\;\;\; 0 \leq u \leq 1, 0 \leq v \leq 1
\tag{1}

B_i^n(t)は、前回の日記で説明したバーンスタイン(Bernstein)多項式である。

\mathbf{q_{ij}}は、平面上に格子状に配置した制御点で、m, nは、それぞれu軸、v軸方向の制御点の次数である。
曲面の四隅は、制御点の四隅を通る。

Pythonのコード例

ベジエ曲面をPythonで記述すると以下のようになる。

import scipy.misc as scm
import numpy as np
import matplotlib.pyplot as plt

def bernstein(n, i, t):
    return scm.comb(n, i) * t**i * (1 - t)**(n-i)

def bezier_patches(m, n, u, v, q):
    p = np.zeros(2)
    for i in range(m + 1):
        for j in range(n + 1):
            p += bernstein(m, i, u) * bernstein(n, j, v) * q[i, j]
    return p

q = np.array([
    [[0, 0], [0, 1], [0, 3], [0, 5]],
    [[1, 1], [1, 2], [1, 4], [1, 6]],
    [[2, 1], [2, 3], [2, 5], [2, 6]],
    [[4, 1], [4, 3], [4, 4], [4, 5]],
    [[5, 0], [6, 1], [6, 3], [5, 5]],
    ])

list1 = []
for u in np.linspace(0, 1, 10):
    list2 = []
    for v in np.linspace(0, 1, 10):
        list2.append(bezier_patches(4, 3, u, v, q))
    list1.append(list2)
P = np.array(list1)

for i in range(10):
    plt.plot(P[i].T[0], P[i].T[1], color='g')
    P_T = np.array([P[j, i] for j in range(10)])
    plt.plot(P_T.T[0], P_T.T[1], color='g')

for i in range(5):
    plt.plot(q[i].T[0], q[i].T[1], '--o', color='b')
for j in range(4):
    q_T = np.array([q[i, j] for i in range(5)])
    plt.plot(q_T.T[0], q_T.T[1], '--o', color='b')

plt.show()

次数m=4, n=3の制御点を配置して、ベジエ曲面を横軸、縦軸方向にそれぞれ10本の線にして描画している。

関数bezier_patchesのfor文は、リスト内包と行列の積を使って記述すると以下のようになる。

def bezier_patches(m, n, u, v, q):
    return np.dot([bernstein(n, j, v) for j in range(n + 1)], np.tensordot([bernstein(m, i, u) for i in range(m + 1)], q, axes=1))
実行結果

f:id:TadaoYamaoka:20170308214747p:plain

緑の線がベジエ曲面、青の点線が制御点の格子を表している。

Pythonでベジエ曲線を描く

Free-Form Deformation(FFD)を使用して2D画像の変形を行おうと試みているが、FFDはベジエ(Bezier)曲面で表される座標系を使用して変形する。
そこで、ベジエ曲線を復習を兼ねてmatplotlibで描画してみる。

ベジエ曲線を2次元に拡張するとベジエ曲面となる。
まずは、単純なベジエ曲線を描画する。

ベジエ曲線の定義

ベジエ曲線は次式で表される曲線である。
\displaystyle
\begin{eqnarray*}
P(t) &=& \sum_{i=0}^{n} B_i^n(t)\mathbf{q_i},\;\;\; 0 \leq t \leq 1 \\
B_i^n(t) &=& {}_nC_i t^i(1 - t)^{n-i}
\end{eqnarray*} \tag{1}

{}_nC_iは二項係数で、\binom{n}{i}と表記する場合もあり、以下の式で展開される。
\displaystyle
{}_nC_i = \binom{n}{i} = \frac{n!}{i!(n-i)!} \tag{2}

B_i^n(t)は、バーンスタイン(Bernstein)多項式と呼ばれる。

\mathbf{q_i}は制御点で、nはベジエ曲線の次数である。
ベジエ曲線は、始点\mathbf{q_0}と終点\mathbf{q_n}を通る。

Pythonのコード例

ベジエ曲線Pythonで記述すると以下のようになる。

import scipy.misc as scm
import numpy as np
import matplotlib.pyplot as plt

def bernstein(n, i, t):
    return scm.comb(n, i) * t**i * (1 - t)**(n-i)

def bezier(n, t, q):
    p = np.zeros(2)
    for i in range(n + 1):
        p += bernstein(n, i, t) * q[i]
    return p

q = np.array([[0, 0], [1, 1], [4, -1], [5, 0]], dtype=np.float)

list = []
for t in np.linspace(0, 1, 100):
    list.append(bezier(3, t, q))
P = np.array(list)

plt.plot(P.T[0], P.T[1])
plt.plot(q.T[0], q.T[1], '--o')
plt.show()

二項係数は、scipy.misc.combを使用して計算している。

上記のコード例では、制御点の次数は3で、制御点(0, 0)、(1, 1)、(4, -1)、(5, 0)によって表されるベジエ曲線を描画している。

なお、list.appendの部分をリスト内包を使ってもう少しスマートに書くと、

P = np.array([bezier(3, t, q) for t in np.linspace(0, 1, 100)])

のように記述できる。

bezier関数でΣの計算にforを使用しているが、以下のようにリスト内包と行列の積を使用しても記述できる。

np.dot([bernstein(n, i, t) for i in range(n + 1)], q)

numpyの行列は、横ベクトルなので、行列の積を計算する際は間違わないように注意が必要である。

実行結果

f:id:TadaoYamaoka:20170307220018p:plain

PyOpenGLを使ってみる その6(テクスチャに頂点配列を使う)

テクスチャに頂点配列を使う。

import sys
from OpenGL.GL import *
from OpenGL.GLU import *
from OpenGL.GLUT import *
from PIL import Image

def load_texture():
    img = Image.open("sample2.png")
    w, h = img.size
    glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, w, h, 0, GL_RGBA, GL_UNSIGNED_BYTE, img.tobytes())

def display():
    glClear(GL_COLOR_BUFFER_BIT)

    glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA)
    glEnable(GL_BLEND)

    glEnableClientState(GL_VERTEX_ARRAY)
    glEnableClientState(GL_TEXTURE_COORD_ARRAY)

    glEnable(GL_TEXTURE_2D)

    glVertexPointerf([
        (-1.0, -1.0, 0.0),
        ( 1.0, -1.0, 0.0),
        (-1.0,  1.0, 0.0),
        ( 1.0,  1.0, 0.0),
        ])
    glTexCoordPointerf([
        (0.0, 1.0),
        (1.0, 1.0),
        (0.0, 0.0),
        (1.0, 0.0),
        ])

    glDrawArrays(GL_TRIANGLE_STRIP, 0, 4)

    glDisable(GL_TEXTURE_2D)

    glDisable(GL_BLEND)

    glFlush()

def main():
    glutInit(sys.argv)
    glutInitDisplayMode(GLUT_RGBA)
    glutInitWindowSize(300, 300)
    glutCreateWindow(b"TextureSample3")
    glutDisplayFunc(display)
    glClearColor(0.0, 0.0, 1.0, 1.0)

    load_texture()
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR)
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR)

    glutMainLoop()

if __name__ == '__main__':
    main()
解説
    glEnableClientState(GL_TEXTURE_COORD_ARRAY)

で、テクスチャの頂点配列を有効にする。

    glTexCoordPointerf([
        (0.0, 1.0),
        (1.0, 1.0),
        (0.0, 0.0),
        (1.0, 0.0),
        ])

で、テクスチャの頂点配列を定義する。
glVertexPointerfで定義したテクスチャを貼るポリゴンの頂点と順番を一致させる。

    glDrawArrays(GL_TRIANGLE_STRIP, 0, 4)

で、テクスチャを描画する。
この例で、GL_TRIANGLE_STRIPで四角形を描画している。

実行結果

f:id:TadaoYamaoka:20170307065527p:plain

PyOpenGLを使ってみる その5(頂点配列にnumpyを使う)

前回は頂点配列にPythonの組み込みのタプルとリストを使用したが、numpyを使うこともできる。
座標変換はOpenGLの機能で行った方が簡単だが、場合によってはnumpyを使用できると便利である。

import sys
from OpenGL.GL import *
from OpenGL.GLU import *
from OpenGL.GLUT import *
import numpy as np

def display():
    glClear(GL_COLOR_BUFFER_BIT)

    glEnableClientState(GL_VERTEX_ARRAY)

    glColor3f(1.0, 0.0, 0.0)

    glVertexPointerf(np.array([
        [ 0.0,  1.0, 0.0],
        [-0.5,  0.0, 0.0],
        [ 0.5,  0.0, 0.0],
        ], dtype=np.float))
    glDrawArrays(GL_TRIANGLES, 0, 3)

    glColor3f(0.0, 1.0, 0.0)
    glVertexPointerf(np.array([
        [-0.5,  0.0, 0.0],
        [-0.5, -1.0, 0.0],
        [ 0.5,  0.0, 0.0],
        [ 0.5, -1.0, 0.0],
        ], dtype=np.float))
    glDrawArrays(GL_TRIANGLE_STRIP, 0, 4)

    glFlush()

def main():
    glutInit(sys.argv)
    glutInitDisplayMode(GLUT_RGBA)
    glutInitWindowSize(300, 300)
    glutCreateWindow(b"sample3")
    glutDisplayFunc(display)
    glClearColor(0.0, 0.0, 1.0, 1.0)

    glutMainLoop()

if __name__ == '__main__':
    main()

glVertexPointerfに渡すndarrayのshapeは、(N, 3)である必要がある。

PyOpenGLを使ってみる その4(頂点配列)

頂点配列を使って描画を行う。

import sys
from OpenGL.GL import *
from OpenGL.GLU import *
from OpenGL.GLUT import *

def display():
    glClear(GL_COLOR_BUFFER_BIT)

    glEnableClientState(GL_VERTEX_ARRAY)

    glColor3f(1.0, 0.0, 0.0)
    glVertexPointerf([
        ( 0.0,  1.0, 0.0),
        (-0.5,  0.0, 0.0),
        ( 0.5,  0.0, 0.0),
        ])
    glDrawArrays(GL_TRIANGLES, 0, 3)

    glColor3f(0.0, 1.0, 0.0)
    glVertexPointerf([
        (-0.5,  0.0, 0.0),
        (-0.5, -1.0, 0.0),
        ( 0.5,  0.0, 0.0),
        ( 0.5, -1.0, 0.0),
        ])
    glDrawArrays(GL_TRIANGLE_STRIP, 0, 4)

    glFlush()

def main():
    glutInit(sys.argv)
    glutInitDisplayMode(GLUT_RGBA)
    glutInitWindowSize(300, 300)
    glutCreateWindow(b"sample2")
    glutDisplayFunc(display)
    glClearColor(0.0, 0.0, 1.0, 1.0)

    glutMainLoop()

if __name__ == '__main__':
    main()
解説

頂点配列を使用するには、

    glEnableClientState(GL_VERTEX_ARRAY)

で、頂点配列を有効にする。

次に、頂点配列を定義する。PyOpenGLでは、glVertexPointerfを使うことで、頂点のタプルのリストをPythonの組み込み型のまま直接使用できる。
上記の例では、三角形の頂点を

    glVertexPointerf([
        ( 0.0,  1.0, 0.0),
        (-0.5,  0.0, 0.0),
        ( 0.5,  0.0, 0.0),
        ])

で定義している。

次に、定義した頂点配列を使用して、glDrawArraysで三角形を描画する。引数にGL_TRIANGLESを指定することで三角形の描画になる。

GL_TRIANGLES以外にもいくつか描画方法がある。詳しくは、マニュアルを参照。

GL_TRIANGLE_STRIP

GL_TRIANGLE_STRIPを使用すると四角形を描画することができる。
4つの頂点を定義して、頂点番号(0, 1, 2)と(1, 2, 3)の2つの三角形を描画する。
上記の例では、

    glVertexPointerf([
        (-0.5,  0.0, 0.0),
        (-0.5, -1.0, 0.0),
        ( 0.5,  0.0, 0.0),
        ( 0.5, -1.0, 0.0),
        ])

で、4つの頂点を定義して、

    glDrawArrays(GL_TRIANGLE_STRIP, 0, 4)

で、GL_TRIANGLE_STRIPを使用して四角を描画している。

実行結果

f:id:TadaoYamaoka:20170305124717p:plain

PyOpenGLを使ってみる その3(移動と回転)

前回表示したテクスチャを移動と回転させる。

import sys
from OpenGL.GL import *
from OpenGL.GLU import *
from OpenGL.GLUT import *
from PIL import Image

def load_texture():
    img = Image.open("sample1.png")
    w, h = img.size
    glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, w, h, 0, GL_RGBA, GL_UNSIGNED_BYTE, img.tobytes())

def display():
    glClear(GL_COLOR_BUFFER_BIT)

    glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA)
    glEnable(GL_BLEND)

    glEnable(GL_TEXTURE_2D)

    glPushMatrix()
    glTranslatef(0.0, 0.5, 0.0)
    glRotatef(90.0, 0.0, 0.0, 1.0)

    glBegin(GL_QUADS)
    glTexCoord2d(0.0, 1.0)
    glVertex3d(-0.5, -0.5,  0.0)
    glTexCoord2d(1.0, 1.0)
    glVertex3d( 0.5, -0.5,  0.0)
    glTexCoord2d(1.0, 0.0)
    glVertex3d( 0.5,  0.5,  0.0)
    glTexCoord2d(0.0, 0.0)
    glVertex3d(-0.5,  0.5,  0.0)
    glEnd()

    glPopMatrix()

    glDisable(GL_TEXTURE_2D)

    glDisable(GL_BLEND)

    glFlush()

def main():
    glutInit(sys.argv)
    glutInitDisplayMode(GLUT_RGBA)
    glutInitWindowSize(300, 300)
    glutCreateWindow(b"TextureSample1")
    glutDisplayFunc(display)
    glClearColor(0.0, 0.0, 1.0, 1.0)

    load_texture()
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR)
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR)

    glutMainLoop()

if __name__ == '__main__':
    main()
解説

表示するテクスチャを貼った四角の座標はローカル座標系で指定するため、行列スタックを使用して、現在の座標系(この例では、ワールド座標系)を保存する。

    glPushMatrix()

ローカル座標系での操作が終わった後、

    glPopMatrix()

で保存時の座標系(この例では、ワールド座標系)に復元する。
この例ではローカル座標系は1つしか扱っていないので、保存しなくても問題ない。
2つ以上のローカル座標系を扱う場合や親子関係のオブジェクトを扱う場合は行列スタックが必要になる。


移動は、

    glTranslatef(0.0, 0.5, 0.0)

で行う。

回転は、

    glRotatef(90.0, 0.0, 0.0, 1.0)

で行う。
この例では、Z軸周りに90度(右回り)回転させて、Y軸方向に0.5移動している。

移動の行列をA_2、回転をA_1、頂点のベクトルを\mathbf{x}とすると、ローカル座標系からワールド座標系への変換(モデリング変換)は、A_2 A_1 \mathbf{x}の式で計算される。
行列の積に交換法則は成立しないので、A_2A_1を逆にできないことに注意する。
回転してから移動と、移動してから回転では、回転の原点が異なることは直観的に理解できる。

補足

初期状態では、モデリング変換は、単位行列になっている。
何か操作を行った後に、単位行列に初期化したい場合は、

    glLoadIdentity()

を実行する。

透視変換の行列の操作をした後は、

    glMatrixMode(GL_MODELVIEW)

モデリング変換の行列を選択することを忘れないようにする。

実行結果

f:id:TadaoYamaoka:20170304162124p:plain