3D 顔演習: Face3D に基づく顔の生成、レンダリング、3D 再構成 <3>

face3d: 3D 顔を処理するための Python ツール

git コード: https://github.com/yfeng95/face3d
論文リスト: PaperWithCode

BFM モデルに基づいて 3DMM のパラメータを推定することで、線形な顔表現を実現でき、キーポイントに基づいた顔生成、姿勢検出、レンダリングに使用できます。お勧め!



3DMMモデルで生成した顔1:通常の表情
ここに画像の説明を挿入
3DMMモデルで生成した顔2:笑顔の表情
ここに画像の説明を挿入

3DMM模型是如何运行的?其原理是怎样的?如何实现三维人脸定制生成呢?
上記の質問に答えるには、3DMM がどのような情報を提供するのかを理解する必要があります。この情報を編集して特定の顔の生成を実現する方法。

1. レビュー

Face3d のソリューション プロセスは次のように要約できます。

  1. α を初期化します。β は 0 です。
  2. ゴールド スタンダード アルゴリズムを使用してアフィン行列 {P_A} を取得し、分解してs 、 R 、 t 2 d {s,R,t_{2d}} を取得します。_R t2
  3. (2) で計算されたs 、 R 、 t 2 ds,R,t_{2d}_R t2それをエネルギー方程式に取り入れてβを解きます。
  4. (2)と(3)で得られたαをエネルギー方程式に代入してαを解きます。
  5. α、βの値を更新し、(2)~(4)を繰り返す反復更新

アルゴリズムの最初の 2 つのステップについては、前のシリーズで説明しました。ここでα、βの解き方を解析していきます。

1.1 (2)で計算したs , R , t 2 ds,R,t_{2d} を計算する_R t2エネルギー方程式にβの解を導入すると、

コード分​​析
前回の記事では、ゴールド スタンダード アルゴリズムを通じてアフィン行列PA P_Aを解きました。Pそしてそれをs 、 R 、 t 2 ds,R,t_{2d}に分解します。_R t2, この部分では、解決策の手順に従ってソース コードの分析を続けます。

for i in range(max_iter):
        X = shapeMU + shapePC.dot(sp) + expPC.dot(ep)
        X = np.reshape(X, [int(len(X)/3), 3]).T
        
        #----- estimate pose
        P = mesh.transform.estimate_affine_matrix_3d22d(X.T, x.T)
        s, R, t = mesh.transform.P2sRt(P)
        rx, ry, rz = mesh.transform.matrix2angle(R)
        # print('Iter:{}; estimated pose: s {}, rx {}, ry {}, rz {}, t1 {}, t2 {}'.format(i, s, rx, ry, rz, t[0], t[1]))

        #----- estimate shape
        # expression
        shape = shapePC.dot(sp)
        shape = np.reshape(shape, [int(len(shape)/3), 3]).T
        ep = estimate_expression(x, shapeMU, expPC, model['expEV'][:n_ep,:], shape, s, R, t[:2], lamb = 0.002)

        # shape
        expression = expPC.dot(ep)
        expression = np.reshape(expression, [int(len(expression)/3), 3]).T
        sp = estimate_shape(x, shapeMU, shapePC, model['shapeEV'][:n_sp,:], expression, s, R, t[:2], lamb = 0.004)

    return sp, ep, s, R, t

式の場合:
ここに画像の説明を挿入
形状部分は∑ i = 1 m α i S i \sum_{i=1}^{m}\alpha_iS_ii = 1メートルある私はS私は
shape = shapePC.dot(sp)
による形状の追加を定義します。
図形の形式は (159645, 1) ですが、
shape = np.reshape(shape, [int(len(shape)/3), 3]).T
図形の XYZ 座標を分離して (53215, 3) 形式に変換します。

ep が β である次のコード部分
ep = estimate_expression(x, shapeMU, expPC, model['expEV'][:n_ep,:], shape, s, R, t[:2], lamb = 0.002)
、estimate_expression のソース コードは次のとおりです。

def estimate_expression(x, shapeMU, expPC, expEV, shape, s, R, t2d, lamb = 2000):
    '''
    Args:
        x: (2, n). image points (to be fitted)
        shapeMU: (3n, 1)
        expPC: (3n, n_ep)
        expEV: (n_ep, 1)
        shape: (3, n)
        s: scale
        R: (3, 3). rotation matrix
        t2d: (2,). 2d translation
        lambda: regulation coefficient

    Returns:
        exp_para: (n_ep, 1) shape parameters(coefficients)
    '''
    x = x.copy()
    assert(shapeMU.shape[0] == expPC.shape[0])
    assert(shapeMU.shape[0] == x.shape[1]*3)

    dof = expPC.shape[1]

    n = x.shape[1]
    sigma = expEV
    t2d = np.array(t2d)
    P = np.array([[1, 0, 0], [0, 1, 0]], dtype = np.float32)
    A = s*P.dot(R) #(2,3)

    # --- calc pc
    pc_3d = np.resize(expPC.T, [dof, n, 3]) 
    pc_3d = np.reshape(pc_3d, [dof*n, 3]) # (29n,3)
    pc_2d = pc_3d.dot(A.T) #(29n,2)
    pc = np.reshape(pc_2d, [dof, -1]).T # 2n x 29

    # --- calc b
    # shapeMU
    mu_3d = np.resize(shapeMU, [n, 3]).T # 3 x n
    # expression
    shape_3d = shape
    # 
    b = A.dot(mu_3d + shape_3d) + np.tile(t2d[:, np.newaxis], [1, n]) # 2 x n
    b = np.reshape(b.T, [-1, 1]) # 2n x 1

    # --- solve
    equation_left = np.dot(pc.T, pc) + lamb * np.diagflat(1/sigma**2)
    x = np.reshape(x.T, [-1, 1])
    equation_right = np.dot(pc.T, x - b)

    exp_para = np.dot(np.linalg.inv(equation_left), equation_right)
    
    return exp_para

次に、estimate_expression 関数を分析します。

  • 情報処理
 x = x.copy()
    assert(shapeMU.shape[0] == expPC.shape[0])
    assert(shapeMU.shape[0] == x.shape[1]*3)

    dof = expPC.shape[1]

    n = x.shape[1]
    sigma = expEV
    t2d = np.array(t2d)
    P = np.array([[1, 0, 0], [0, 1, 0]], dtype = np.float32)

まず、入力の形式が正しいことを確認します。
assert(shapeMU.shape[0] == expPC.shape[0])
assert(shapeMU.shape[0] == x.shape[1]*3)
このとき入力される式の主成分 expPC 形式は (159645,29)
let dof=29
dof = expPC.shape[1]
let n=68であり
n = x.shape[1]
、式の主成分の分散である sigma=expEV になります。 σ
sigma = expEV
t 2 d t_{2d}t2直交射影行列P orth P_{orth}
t2d = np.array(t2d)
である配列 array P に変換します。Pまたは_ _
P = np.array([[1, 0, 0], [0, 1, 0]], dtype = np.float32)

	A = s*P.dot(R) #(2,3)

    # --- calc pc
    pc_3d = np.resize(expPC.T, [dof, n, 3]) 
    pc_3d = np.reshape(pc_3d, [dof*n, 3]) # (29n,3)
    pc_2d = pc_3d.dot(A.T) #(29n,2)
    pc = np.reshape(pc_2d, [dof, -1]).T # 2n x 29

    # --- calc b
    # shapeMU
    mu_3d = np.resize(shapeMU, [n, 3]).T # 3 x n
    # expression
    shape_3d = shape
    # 
    b = A.dot(mu_3d + shape_3d) + np.tile(t2d[:, np.newaxis], [1, n]) # 2 x n
    b = np.reshape(b.T, [-1, 1]) # 2n x 1

既知の公式は、
ここに画像の説明を挿入
A、pc、および b を定義および計算します。

  • 定義 A:
    A = s*P.dot(R)
    ここに画像の説明を挿入
  • pc の計算:ここでの pc の計算は、式主成分 expPC を (29, 68, 3) の新しい行列 pc_3d に変換する次
    の式と同等です。
    ここに画像の説明を挿入

    pc_3d = np.resize(expPC.T, [dof, n, 3])

注: ここでの expPC は
expPC = model['expPC'][valid_ind, :n_ep]
計算されており、特徴点の式主成分のみが含まれています。pc_3d を (29 68,3) 形式に変換するには、形式は (68 3,29)

pc_3d = np.reshape(pc_3d, [dof*n, 3])
です。計算: pc 2 d = pc 3 d ⋅ AT pc_{2d}= pc_{3d}\cdot A^Tパソコン_2=パソコン_3D_ _T、pc_2d 形式は (29*68, 2) です。
pc_2d = pc_3d.dot(A.T)

pc_2d を展開した後、pc を取得します。
pc = np.reshape(pc_2d, [dof, -1]).T

  • b b を定義する
    式は次のとおりです:
    ここに画像の説明を挿入
    行列の形式により、いくつかの変換を最初に実行する必要があります:
    ここのshapeMU にも 68 個の特徴点のみが含まれています。
    形式 (68*3,1) のshapeMU は次のように変換されます。形式 (3, 68):
    mu_3d = np.resize(shapeMU, [n, 3]).T
    ここで、shape =shapePC.dot(sp)、つまり、∑ i = 1 mai S i \sum_{i=1}^{m}a_iS_ii = 1メートルある私はS私は
    shape_3d = shape

この時点で、b は式に従って計算できます。得られた b の形式は (2,68) であり、
b = A.dot(mu_3d + shape_3d) + np.tile(t2d[:, np.newaxis], [1, n])
b は (68*2,1) の形式に変換されます。
b = np.reshape(bT, [-1, 1])


A、pc、b の定義と計算が完了したらβ を計算しますX 投影 X_{projection}バツプロジェクト_ _ _ _ _ _ _の式は次のように書くことができます。
ここに画像の説明を挿入
PC に取り込んだ式は次のように書くことができます。
ここに画像の説明を挿入

X 投影 X_{projection}バツプロジェクト_ _ _ _ _ _ _β の式をエネルギー方程式に取り入れます。β
ここに画像の説明を挿入
の微分値を取得し、微分値がゼロになるときの β の値を取得します。
L2 ノルムの導出は、次の式を使用して取得できます。
ここに画像の説明を挿入
取得:
ここに画像の説明を挿入
取得を簡略化します:
ここに画像の説明を挿入
次に、β を取得するコードです。

equation_left = np.dot(pc.T, pc) + lamb * np.diagflat(1/sigma**2)
x = np.reshape(x.T, [-1, 1])
equation_right = np.dot(pc.T, x - b)
exp_para = np.dot(np.linalg.inv(equation_left), equation_right)

1.2 (4). (2)と(3)で得られたβをエネルギー方程式に代入してαを解きます。

同様に、αを取得するコードは次のとおりです。

expression = expPC.dot(ep)
expression = np.reshape(expression, [int(len(expression)/3), 3]).T
sp = estimate_shape(x, shapeMU, shapePC, model['shapeEV'][:n_sp,:], expression, s, R, t[:2], lamb = 0.004)

アルゴリズムのプロセスはβの計算と同じですが、取り込まれるβは上記の計算後の新しい値です。

1.3 (5) α、βの値を更新し、(2)~(4)を繰り返し更新する

ここでループの繰り返し部分のコードは終了し、複数回の繰り返し(プログラムで指定した繰り返し回数は3回)を繰り返すことで、必要なsp、ep、s、R、tが得られます。

日常に戻る

bfm.fit に戻り、
fitted_sp, fitted_ep, s, R, t = fit.fit_points(x, X_ind, self.model, n_sp = self.n_shape_para, n_ep = self.n_exp_para, max_iter = max_iter)
下方向に実行を続けます。

def fit(self, x, X_ind, max_iter = 4, isShow = False):
        ''' fit 3dmm & pose parameters
        Args:
            x: (n, 2) image points
            X_ind: (n,) corresponding Model vertex indices
            max_iter: iteration
            isShow: whether to reserve middle results for show
        Returns:
            fitted_sp: (n_sp, 1). shape parameters
            fitted_ep: (n_ep, 1). exp parameters
            s, angles, t
        '''
        if isShow:
            fitted_sp, fitted_ep, s, R, t = fit.fit_points_for_show(x, X_ind, self.model, n_sp = self.n_shape_para, n_ep = self.n_exp_para, max_iter = max_iter)
            angles = np.zeros((R.shape[0], 3))
            for i in range(R.shape[0]):
                angles[i] = mesh.transform.matrix2angle(R[i])
        else:
            fitted_sp, fitted_ep, s, R, t = fit.fit_points(x, X_ind, self.model, n_sp = self.n_shape_para, n_ep = self.n_exp_para, max_iter = max_iter)
            angles = mesh.transform.matrix2angle(R)
        return fitted_sp, fitted_ep, s, angles, t

回転行列を XYZ 角度に変換したangles = mesh.transform.matrix2angle(R)
後、fitt_sp、fitt_ep、s、angle、t を返します。

3DMM ルーチンに戻り、
fitted_sp, fitted_ep, fitted_s, fitted_angles, fitted_t = bfm.fit(x, X_ind, max_iter = 3)
実行が完了したら、引き続き実行します。

x = projected_vertices[bfm.kpt_ind, :2] # 2d keypoint, which can be detected from image
X_ind = bfm.kpt_ind # index of keypoints in 3DMM. fixed.

# fit
fitted_sp, fitted_ep, fitted_s, fitted_angles, fitted_t = bfm.fit(x, X_ind, max_iter = 3)

# verify fitted parameters
fitted_vertices = bfm.generate_vertices(fitted_sp, fitted_ep)
transformed_vertices = bfm.transform(fitted_vertices, fitted_s, fitted_angles, fitted_t)

image_vertices = mesh.transform.to_image(transformed_vertices, h, w)
fitted_image = mesh.render.render_colors(image_vertices, bfm.triangles, colors, h, w)

次のステップでは、
fitted_vertices = bfm.generate_vertices(fitted_sp, fitted_ep)
計算された α と β を代入してS new Model S_{newModel}
ここに画像の説明を挿入
を計算します。S新しいモデル_ _ _ _ _、対応するソースコードは次のとおりです。

 def generate_vertices(self, shape_para, exp_para):
        '''
        Args:
            shape_para: (n_shape_para, 1)
            exp_para: (n_exp_para, 1) 
        Returns:
            vertices: (nver, 3)
        '''
        vertices = self.model['shapeMU'] + \
                   self.model['shapePC'].dot(shape_para) + \
                   self.model['expPC'].dot(exp_para)
        vertices = np.reshape(vertices, [int(3), int(len(vertices)/3)], 'F').T

        return vertices

新しいモデル S_{newModel}を計算しますS新しいモデル_ _ _ _ _次に、3D モデルに対して同様の変換を実行します。
transformed_vertices = bfm.transform(fitted_vertices, fitted_s, fitted_angles, fitted_t)

S 変換 = s ⋅ R ⋅ S 新しいモデル + t 3 d S_{変換済み}=s\cdot R\cdot S_{新しいモデル}+t_{3d}S医療輸送_ _ _ _ _=sRS新しいモデル_ _ _ _ _+t3D_ _

 def transform(self, vertices, s, angles, t3d):
        R = mesh.transform.angle2matrix(angles)
        return mesh.transform.similarity_transform(vertices, s, R, t3d)

以下の後のコード:

image_vertices = mesh.transform.to_image(transformed_vertices, h, w)
fitted_image = mesh.render.render_colors(image_vertices, bfm.triangles, colors, h, w)

3D モデルを 2D 画像形式に変換し、組み込まれた色情報を使用してモデルに色を付けていますが、ここではあまり説明しません。

結果表示
生成された新規顔画像はresults/3dmmディレクトリに保存されます

# ------------- print & show 
print('pose, groudtruth: \n', s, angles[0], angles[1], angles[2], t[0], t[1])
print('pose, fitted: \n', fitted_s, fitted_angles[0], fitted_angles[1], fitted_angles[2], fitted_t[0], fitted_t[1])

save_folder = 'results/3dmm'
if not os.path.exists(save_folder):
    os.mkdir(save_folder)

io.imsave('{}/generated.jpg'.format(save_folder), image)
io.imsave('{}/fitted.jpg'.format(save_folder), fitted_image)

特徴点フィッティングのプロセスを示す gif を生成することもできます。

# fit

fitted_sp, fitted_ep, fitted_s, fitted_angles, fitted_t = bfm.fit(x, X_ind, max_iter = 3, isShow = True)

# verify fitted parameters
for i in range(fitted_sp.shape[0]):
	fitted_vertices = bfm.generate_vertices(fitted_sp[i], fitted_ep[i])
	transformed_vertices = bfm.transform(fitted_vertices, fitted_s[i], fitted_angles[i], fitted_t[i])

	image_vertices = mesh.transform.to_image(transformed_vertices, h, w)
	fitted_image = mesh.render.render_colors(image_vertices, bfm.triangles, colors, h, w)
	io.imsave('{}/show_{:0>2d}.jpg'.format(save_folder, i), fitted_image)

options = '-delay 20 -loop 0 -layers optimize' # gif. need ImageMagick.
subprocess.call('convert {} {}/show_*.jpg {}'.format(options, save_folder, save_folder + '/3dmm.gif'), shell=True)
subprocess.call('rm {}/show_*.jpg'.format(save_folder), shell=True)

結果の表示は次のとおりです。

  • 生成された.jpg
    ここに画像の説明を挿入

  • フィットした.jpg
    ここに画像の説明を挿入

  • 3dmm.gif

ここに画像の説明を挿入
もちろん、プログラムによって生成された新しいランダム モデルは、実行されるたびに異なって見えます。


要約する

ここでは 3DMM モデルの逆過程に基づいて α と β を解く過程を中心に紹介します。公式と原則を理解し、その公式を組み合わせてコードの読み取りをガイドするには、多くの詳細をコードと比較する必要があります。

おすすめ

転載: blog.csdn.net/wqthaha/article/details/129420854