【古典的な画像ワーピング手法】Thin Plate Spline:TPSの理論とコードを詳しく解説

0. 序文

2022年は新しいブログは書かず、論文執筆を中心に活動していきますので、今年から本格的に回復していきます!

この記事の目的は、ランドマーク (記事の後半ではコントロール ポイントと呼ばれることもあります) に基づいた古典的な画像ワーピング (歪み/変形) アルゴリズムである Thin Plate Spine (TPS) を詳細に分析することです。

TPS はさまざまなタスク、特に人間の顔、動物の顔などの生物学的形状で広く使用されています TPS は 3 次スプラインの 2D 一般化形式です 画像処理で一般的に使用されることは注目に値します アフィン変換は特別なバリアントとして理解できますTPSの。

  • 画像の歪み/変形の問題とは何ですか? 2 つの画像に[3]
    対応する制御点 (図の緑色の接続線で示されているランドマーク) がいくつかあるとすると、画像 A (参照画像)に特定の変形を実行して、その制御点が次のようになります。比較画像B(対象テンプレート)のランドマークが重なっています。

TPS は最も古典的な手法の 1 つであり、その基本的な前提条件は次のとおりです。
如果用一个薄钢板的形变来模拟这种2D形变, 在确保landmarks能够尽可能匹配的情况下,怎么样才能使得钢板的弯曲量(deflection)最小。

  • 使用例
    TPS アルゴリズムの私の実践での使用方法は、画像のランドマーク(下図の左側の黒い三角形) に従って、マッピング関係(緑色の接続線)に従って2D 画像を論理的にワーピング (ワーピング) します。ターゲット テンプレート(下図の右側) )にコピーします。
    ここに画像の説明を挿入します

1. 理論

Thin-Plate-Spline、この記事の残りの部分はその略語TPSに置き換えられます。TPS は実際には数学的な概念です[1]

TPS は 1D 3 次スプラインの 2 次元シミュレーションであり、二調和方程式(Biharmonic Equation)の基本解であり[2]、その形式は次のとおりです。

U ( r ) = r 2 ln ⁡ ( r ) U(r) = r^2 \ln(r)U ( r )=r2ln ( r )

1.1 U ( r ) U(r)U ( r )の形の起源

では、なぜこのような形になっているのでしょうか? [10]1989 年に Bookstein によって出版された論文「Principle Warps: Thin-Plate Splines and the Decomposition of Deformation」では、彼はそれを二調和方程式の基本解法で拡張しました。

まず、rrrはx 2 + y 2 \sqrt{x^2+y^2}を表しますバツ2+y2 (デカルト座標系)、論文ではブックスタインはU ( r ) = − r 2 ln ⁡ ( r ) U(r) = -r^2 \ln(r) を使用しています。U ( r )=r2ln ( r ) 、その目的は視覚化の便宜のためこのポーズでは、上から見るとわずかにへこんでいるように見えますが、それ以外の場合は凸面になります(つまり、中心 X 点付近の領域がへこんでいるように見えます)。).
ここに画像の説明を挿入します
この関数は当然次の方程式を満たします。

Δ 2 U = ( ∂ 2 ∂ x 2 + ∂ 2 ∂ y 2 ) 2 U ∝ δ ( 0 , 0 ) \Delta^2U = (\frac{\partial ^{2}}{\partial x^{2} } + \frac{\partial ^{2}}{\partial y^{2}})^2 U \propto \delta_{(0,0)}D2U _=(×22+∂y _22)2U _d( 0 , 0 )

式の左辺と(0,0) \delta_{(0,0)} の関数δ (0, 0)d( 0 , 0 )同等 (関数の導入は次のとおり)、δ ( 0 , 0 ) \delta_{(0,0)}d( 0 , 0 )これは (0,0) を除く他の位置では 0 になる関数であり、その積分は 1 になります (ディラック デルタ関数はこの関数の形式として理解されるべきだと思います)。

したがって、二調和方程式の形式はΔ 2 U = 0 \Delta^2U=0であるため、D2U _=0の場合、明らかに、U ( r ) = ( ± ) r 2 ln ⁡ ( r ) U(r) = (\pm) r^2 \ln(r)U ( r )=( ± ) r2ln ( r )はすべてこの条件を満たすため、二調和関数の基本解

簡単に言うと、関数は、定義域が関数のセットであり、その値の範囲が実数または複素数であるマッピングです。関数の例はZhihuから借用しています: 2D 平面上の 2 点間の直線距離図に示すように、2次元平面空間において、座標原点(0,0)から点(a,b)までの接続曲線は、y = y ( x ) y = y(x)となります。[11]
ここに画像の説明を挿入します
y=y ( x )、および接続曲線の微小要素Δ \DeltaΔまたはds = 1 + (dydx ) 2 dx ds = \sqrt{1+(\frac{dy}{dx})^2dx}ds _=1+(dx _やあ_)2dx __ 、全長はds dsです。ds [ 0 , a ] [0, a] [ 0 ,a ]の積分
: s = ∫ 0 a ( 1 + y ′ 2 ) 1 / 2 dxs = \int_{0}^{a}(1+y^{'2})^{1/2}dxs=0( 1+y' 2)1/2 dxここ、sssスカラーy ' ( x ) y^{'}(x)y' (x)汎関数でありs ( y ' ) s(y^{'})とも書かれます。s (' ). すると、上記の問題は次のようになります: 曲線y ( x ) y(x)y ( x )は関数s ( y ' ) s(y^{'})s (' )最小限。

わかりました、UUUの起源と定義が明確になったので、私たちの目標は次のとおりです。

サンプル ポイントのセットが与えられると、各サンプル ポイントを中心とする薄板スプライン (TPS) の重み付けされた組み合わせにより、いわゆる曲げエネルギーを最小限に抑えながら、これらのポイントを正確に通過する内挿関数が得られます

では、曲げエネルギーとは何でしょうか?

1.2 曲げエネルギー: 曲げエネルギー

によれば、ここでの曲げエネルギーは、実数体R 2 R^2[1]に関する二次導関数の 2 乗として定義されます。R2 (私の意見では、ここではR 2 R^2R2 は、 2D 画像の高さと幅の積分として直接理解できます

I [ f ( x , y ) ] = ∬ ( fxx 2 + 2 fxy 2 + fyy 2 ) dxdy I[f(x, y)] = \iint (f_{xx}^2 + 2f_{xy}^2+ f_{yy}^2)dxdyI [ f ( x ,y )]=( fxx2+2f _2+fやあ2) d x d y

最適化の目標は、I [ f ( x , y ) ] I[f(x, y)]にすることです。I [ f ( x ,y )]を最小化します。

さて、曲げエネルギーの数学的定義はこれで終わりですが、当然、次のような疑問が生じます。

  • f ( x , y ) f(x, y)f ( x ,y )はどのように定義されますか?
  • 画像のような 2D 平面の場合、曲げエネルギーを最小限に抑えるためにスプラインの重み付けを組み合わせた後の曲げ方向はどうあるべきですか?

まず、曲げの方向の問題を解析して1.4でf (x, y) f(x, y)を実行してみます。f ( x ,y )定義の紹介。

1.3 曲げ方向

まず、TPS の名前をおさらいしてみましょう。TPS は、薄い金属シートを曲げることという物理的なアナロジーに由来しています。

物理用語では、曲げ(たわみ)の方向はzzです。z軸は 2D 画像平面に垂直な軸であり、
この考え方を実際の座標変換の問題に適用するには、TPS を平板を上げ下げし、その上げ下げした平面を 2D に投影すると画像平面は、参照画像と対象テンプレートのランドマーク対応関係に基づいてワーピング(変形)した後の画像結果です。

以下のように、プレーン上に 4 つのコントロール ポイントを設定し、最後の 1 つはエッジ コーナー ポイントではないので、引っ張ると、プレーンは自然に部分的に盛り上がったり下がったりする効果が得られます。
画像の説明を追加してください

明らかに、この種のワープは、下図に示すように、参照ランドマークの赤いXXを考慮すると、ある程度の座標変換でもあります。Xとターゲット ポイントの青色⚪ ⚪ . TPS ワーピングはこれらのXXX は⚪ ⚪に完全に移動します⚪上

ここに画像の説明を挿入します

ここで質問が来ます。次に、このX → ⚪ X \rightarrow ⚪バツモバイル ソリューションはどのように実装されますか?

1.4 2D 平面の座標変換 (別名ワーピング) を実装するにはどうすればよいですか?

以下に示すように[7]、2D 平面上の座標変換は実際には 2 方向の変化です: X \mathbf{X}XY \mathbf{Y}Y方向これら 2 つの方向の変化を実現するための TPS のアプローチは次のとおりです。

2 つのスプライン関数を使用してそれぞれX \mathbf{X}を考慮しますXY \mathbf{Y}Y方向の変位

TPS actually use two splines, 
one for the displacement in the X direction 
and one for the displacement in the Y direction

ここに画像の説明を挿入します
これら 2 つのスプライン関数の定義は次のとおりです[7]( NNN は、上の図に示すように、対応するランドマークの数を指します。N= 5 N=5N=5 ):
ここに画像の説明を挿入します

各方向 ( X, Y \mathbf{X}, \mathbf{Y}に注意してください)× Y )Function(Δ X , Δ Y \mathbf{\Delta X}, \mathbf{\Delta Y}ΔXΔY ) はNNとして見ることができますN点の高さマップ (高さマップ)なので、スプラインは下図に示すように点を[7]
ここに画像の説明を挿入します
スプライン関数の定義式では、

  • 前3个系数 a 1 , a x , a y a_1, a_x, a_y ある1ある×あるはい線形空間の一部(線部分)を表し、 XXを線形空間に当てはめるために使用しますX (xi , yi x_i , y_iバツ私はy私は) と⚪ ⚪ (xi ' , yi ' x_i^{'}, y_i^{'}バツy)。
  • 次の係数wi , i ∈ [ 1 , N ] w_i, i \in [1, N]w私は[ 1 N ] は各制御点を表しますiカーネル重み。制御点XXX (xi , yi x_i , y_iバツ私はy私は) とその最後のx、yx、y× y間の変位
  • 最后的一项是 U ( ∣ ∣ ( x i , y i ) − ( x , y ) ∣ ∣ ) U(|| (x_i, y_i) - (x, y) ||) U ( ∣∣ ( x私はy私は)( x ,y ) ∣∣ )、これは制御点XXX (xi , yi x_i , y_iバツ私はy私は) とその最後のx、yx、y× y間の変位。U ( ∣ ∣ ( xi , yi ) − ( x , y ) ∣ ∣ ) U(|| (x_i, y_i) - (x, y) ||) であることに注意してくださいU ( ∣∣ ( x私はy私は)( x ,y ) ∣∣ ) はL2ノルムを使用します[8]U定义如下: U ( r ) = r 2 ln ⁡ ( r ) U(r) = r^2 \ln(r) U ( r )=r2ln ( r )ここで、TPS の RBF 関数 (動径基底関数)を再検討する必要があります: U ( r ) = r 2 ln ⁡ ( r ) U(r) = r^2 \ln(r)U ( r )=r2ln ( r )上記によれば[9]、RBFのようなガウシアンカーネルは類似性を測定する方法(類似性測定)

1.5 具体的な計算計画

各方向について ( X , Y \mathbf{X}, \mathbf{Y}× Yのスプライン関数のa 1 、 ax 、 ay 、 wi a_1、 a_x、 a_y、 w_iある1ある×あるはいw私はは、次の線形システムを解くことによって取得できます。
ここに画像の説明を挿入します
ここで、K ij = U ( ∣ ∣ ( xi , yi ) − ( xj , yj ) ∣ ∣ ) K_{ij} = U(|| (x_i, y_i) - ( x_j 、 y_j) ||)Kイジ=U ( ∣∣ ( x私はy私は)( ×jyj) ∣∣ )PPピーズ_行iは同次表現( 1 , xi , yi ) (1, x_i, y_i)( 1 バツ私はy私は)○○Oはすべて 0 の 3x3 行列です。oooは 3x1 のすべてゼロの列ベクトルです、www v v v w i w_i w私は v i v_i v私は.aaで構成される列ベクトルa是由 [ a 1 , a x , a y ] [a_1, a_x, a_y] [ _1ある×あるはい]列ベクトルで構成されます。

具体的には、左側の大きな行列の形式は次のようになります[9-10]
ここに画像の説明を挿入します
N=3 (制御点の数は 3) を例にとると、X \mathbf{X}注: [ U 11 U 21 U 31 1 x 1 y 1 U 12 U
22 U 32 1 x 2 y 2 U 13 U 23 U 33 1 x 3 y 0 0 x 1 x 2 x 3 0 0 0 y 1 y 2 y 3 0 0 0 ] × [ w 1 w 2 w 3 a 1 axay ] = [ x 1 ' x 2 ' x 3 ' 0 0 0 ] \begin {bmatrix} U_{11} & U_{21} & U_{ 31} & 1 & x_1 & y_1\\ U_{12} & U_{22} & U_{32} & 1 & x_2 & y_2\\ U_{1 } & U_{23} & U_{33} & 1 & x_3 & y_3 \\ 1 & 1 & 1 & 0 & 0 & 0 \ \ x_1 & x_2 & x_3 & 0 & 0 & 0 \ \ y_1 & y_2 & y_3 & & 0& 0 \end{bmatrix} \times \begin{bmatrix } w_1 \\ w_2 \\ w_3 \\ a_1 \\ a_x \\ a_y \end{bmatrix} = \begin{bmatrix} x_1^{'} \\x_2 ^{'}\\x_3^{'}\\0 \\0\\0\end{bマトリックス} U11U12U131バツ1y1U21U22U231バツ2y2U31U32U331バツ3y3111000バツ1バツ2バツ3000y1y2y3000 × w1w2w3ある1ある×あるはい = バツ1バツ2バツ3000

同様に、Y \mathbf{Y}Yのスプライン関数の線形行列式は次

[ U 11 U 21 U 31 1 x 1 y 1 U 12 U 22 U 32 1 x 2 y 2 U 13 U 23 U 33 1 x 3 y 3 1 1 1 0 0 0 x 1 x 2 x 3 0 0 0 y 1 y 2 y 3 0 0 0 ] × [ w 1 w 2 w 3 a 1 axay ] = [ y 1 ' y 2 ' y 3 ' 0 0 0 ] \begin{bmatrix} U_{11} & U_{21} & U_{31} & 1 & x_1 & y_1\\ U_{12} & U_{22} & U_{32} & 1 & x_2 & y_2\\ U_{13} & U_{23} & U_{33} & 1 & x_3 & y_3 \\ 1 & 1 & 1 & 0 & 0& 0 \\ x_1 & x_2 & x_3 & 0 & 0& 0 \\ y_1 & y_2 & y_3 & 0 & 0& 0 \end{bmatrix} \times \begin {bmatrix} w_1 \\ w_2 \\ w_3 \\ a_1 \\ a_x \\ a_y \end{bmatrix} = \begin{bmatrix} y_1^{'} \\ y_2^{'} \\ y_3^{'} \ \ 0 \\ 0 \\ 0 \end{bmatrix} U11U12U131バツ1y1U21U22U231バツ2y2U31U32U331バツ3y3111000バツ1バツ2バツ3000y1y2y3000 × w1w2w3ある1ある×あるはい = y1y2y3000

N+3 個の未知量を解くために N+3 個の関数が使用されることは明らかであり、対応する[ wa ] \begin{bmatrix} w \\ a \end{bmatrix} を取得できます。[w]

2. コードの実装

私が使用する TPS は cheind/py-thin-plate-spline プロジェクトです[6]。式と実装の対応を理解するために、ここでコードを詳細に分解します。

2.1 コアコンピューティングロジック

コアロジックは関数warp_image_cv:およびにありtps.tps_theta_from_points最も基本的なサンプル コードは次のとおりです。tps.tps_gridtps.tps_grid_to_remap


def show_warped(img, warped, c_src, c_dst):
    fig, axs = plt.subplots(1, 2, figsize=(16,8))
    axs[0].axis('off')
    axs[1].axis('off')
    axs[0].imshow(img[...,::-1], origin='upper')
    axs[0].scatter(c_src[:, 0]*img.shape[1], c_src[:, 1]*img.shape[0], marker='^', color='black')
    axs[1].imshow(warped[...,::-1], origin='upper')
    axs[1].scatter(c_dst[:, 0]*warped.shape[1], c_dst[:, 1]*warped.shape[0], marker='^', color='black')
    plt.show()

def warp_image_cv(img, c_src, c_dst, dshape=None):
    dshape = dshape or img.shape
    theta = tps.tps_theta_from_points(c_src, c_dst, reduced=True)
    grid = tps.tps_grid(theta, c_dst, dshape)
    mapx, mapy = tps.tps_grid_to_remap(grid, img.shape)
    return cv2.remap(img, mapx, mapy, cv2.INTER_CUBIC)

img = cv2.imread('test.jpg')

c_src = np.array([
    [0.44, 0.18],
    [0.55, 0.18],
    [0.33, 0.23],
    [0.66, 0.23],
    [0.32, 0.79],
    [0.67, 0.80],
])

c_dst = np.array([
    [0.693, 0.466],
    [0.808, 0.466],
    [0.572, 0.524],
    [0.923, 0.524],
    [0.545, 0.965],
    [0.954, 0.966],
])


warped_front = warp_image_cv(img, c_src, c_dst, dshape=(512, 512))
show_warped(img, warped1, c_src_front, c_dst_front)

ここに画像の説明を挿入します
このオープン ソース コードには numpy と torch の 2 つのバージョンがあります。ここでは、GPU を持たない友人がハンズオン テストを実行できるように、私の分析は numpy バ​​ージョンで実行されます。

コアTPS

class TPS:       
  @staticmethod
   def fit(c, lambd=0., reduced=False):        
      n = c.shape[0]

      U = TPS.u(TPS.d(c, c))
      K = U + np.eye(n, dtype=np.float32)*lambd

      P = np.ones((n, 3), dtype=np.float32)
      P[:, 1:] = c[:, :2]
      v = np.zeros(n+3, dtype=np.float32)
      v[:n] = c[:, -1]

       A = np.zeros((n+3, n+3), dtype=np.float32)
       A[:n, :n] = K
       A[:n, -3:] = P
       A[-3:, :n] = P.T
       theta = np.linalg.solve(A, v) # p has structure w,a
       return theta[1:] if reduced else thete      
       ...
   @staticmethod
   def z(x, c, theta):
       x = np.atleast_2d(x)
       U = TPS.u(TPS.d(x, c))
       w, a = theta[:-3], theta[-3:]
       reduced = theta.shape[0] == c.shape[0] + 2
       if reduced:
           w = np.concatenate((-np.sum(w, keepdims=True), w))
       b = np.dot(U, w)
       return a[0] + a[1]*x[:, 0] + a[2]*x[:, 1] + b

2.2tps.tps_theta_from_points

この関数の機能は、スプライン関数の[ wa ] \begin{bmatrix} w \\ a \end{bmatrix} を解くことです。[w]
ここに画像の説明を挿入します

def tps_theta_from_points(c_src, c_dst, reduced=False):
    delta = c_src - c_dst
    
    cx = np.column_stack((c_dst, delta[:, 0]))
    cy = np.column_stack((c_dst, delta[:, 1]))
        
    theta_dx = TPS.fit(cx, reduced=reduced)
    theta_dy = TPS.fit(cy, reduced=reduced)

    return np.stack((theta_dx, theta_dy), -1)
  1. デルタは、参照画像の制御点とターゲット テンプレートの制御点の間の補間Δ xi 、 Δ yi \Delta x_i、\D​​elta y_iΔx_ _私はΔy_ _私は

  2. cxcyは に基づいており、c_dstそれぞれΔ xi \Delta x_iが追加されます。Δx_ _私はΔyi \Delta y_iΔy_ _私はの列ベクトル

  3. theta_dxtheta_dyの Reduce パラメーターはデフォルトで False/True に設定され、結果は長さ 9/8 の1D ベクトルになります。計算プロセスにはfitTPS コア クラスの機能が必要です。

TPS.d(cx, cx, reduced=True)またはTPS.d(cy, cy, reduced=True) L2を計算する

    @staticmethod
    def d(a, b):
        # a[:, None, :2] 是把a变成[N, 1, 2]的tensor/ndarray
        # a[None, :, :2] 是把a变成[1, N, 2]的tensor/ndarray
        return np.sqrt(np.square(a[:, None, :2] - b[None, :, :2]).sum(-1))

その機能は、∣ ∣ ( xi , yi ) − ( x , y ) ∣ ∣ || (x_i, y_i) - (x, y) || を計算することです。∣∣ ( x私はy私は)( x ,y ) ∣∣ (L2)、結果は形状がN、NN、NNさんの中間結果です。
ここに画像の説明を挿入します

TPS.u(...) 计算 U ( . . . ) U(...) ( ... )

式とまったく同じです: U ( r ) = r 2 ln ⁡ ( r ) U(r) = r^2 \ln(r)U ( r )=r2ln ( r ) 、 rr を防ぐためrが小さすぎるため、イプシロン係数が追加されます1 e − 6 1e^{-6}1e _6.このステップでは、KKK、形状はN、NN、NN、①と同じ。
ここに画像の説明を挿入します

def u(r):
        return r**2 * np.log(r + 1e-6)

③ と によればcxcy単純なスプライシングで生成できますP
ここに画像の説明を挿入します

	P = np.ones((n, 3), dtype=np.float32)
	P[:, 1:] = c[:, :2] # c就是cx or cy.

④ 根据 Δ x i \Delta x_i Δx_ _私は(同じ方法でcxベクトルの最後の列を取得します)、 vvを取得しますcyv
ここに画像の説明を挿入します

    # c = cx or cy
    v = np.zeros(n+3, dtype=np.float32)
    v[:n] = c[:, -1]

⑤アセンブリ行列A、つまり[10]論文中のLLLマトリックス。
ここに画像の説明を挿入します

     A = np.zeros((n+3, n+3), dtype=np.float32)
     A[:n, :n] = K
     A[:n, -3:] = P
     A[-3:, :n] = P.T

⑥今LLLYYYは既知です、Y = [ vo ] Y = \begin{bmatrix}v \\ o \end{bmatrix}Y=[vああ], 那么 W W WWAa 1 、 ax 、 ay a_1、 a_x、a_yある1ある×あるはいのベクトルは直接線形的に解くことができます
ここに画像の説明を挿入します
[ wa ] \begin{bmatrix}w \\ a \end{bmatrix}[w] =L − 1 L^{-1}L1Y

class TPS:       
    @staticmethod
    def fit(c, lambd=0., reduced=False):
        # 1. TPS.d
        U = TPS.u(TPS.d(c, c))
        K = U + np.eye(n, dtype=np.float32)*lambd

        P = np.ones((n, 3), dtype=np.float32)
        P[:, 1:] = c[:, :2]

        v = np.zeros(n+3, dtype=np.float32)
        v[:n] = c[:, -1]

        A = np.zeros((n+3, n+3), dtype=np.float32)
        A[:n, :n] = K
        A[:n, -3:] = P
        A[-3:, :n] = P.T

        theta = np.linalg.solve(A, v) # p has structure w,a
        return theta[1:] if reduced else theta
        
    @staticmethod
    def d(a, b):
        return np.sqrt(np.square(a[:, None, :2] - b[None, :, :2]).sum(-1))

    @staticmethod
    def u(r):
        return r**2 * np.log(r + 1e-6)

ここに画像の説明を挿入します

thetaつまり、関数は[ wa ] \begin{bmatrix}w \\ a \end{bmatrix}を返します。[w] . これは両方向 (X, Y) で必要なthetaので、

theta = tps.tps_theta_from_points(c_src, c_dst)

返されるシータは(N + 3, 2) (N+3, 2)です。( N+3 2 形。

2.3tps.tps_grid

この関数は、x 方向と y 方向のイメージ プレーンのオフセットを解決します。

def warp_image_cv(img, c_src, c_dst, dshape=None):
    dshape = dshape or img.shape
    # 2.2
    theta = tps.tps_theta_from_points(c_src, c_dst, reduced=True)
    # 2.3
    grid = tps.tps_grid(theta, c_dst, dshape)
    # 2.4
    mapx, mapy = tps.tps_grid_to_remap(grid, img.shape)
    return cv2.remap(img, mapx, mapy, cv2.INTER_CUBIC)

thetaコア コード部分から、 が見つかった場合、つまり[ wa ] \begin{bmatrix}w \\ a \end{bmatrix} であることがわかります。[w]tps_grid .以下の関数を使用して、

機能は次のとおりです。

def tps_grid(theta, c_dst, dshape):
    # 1) uniform_grid(...)    
    ugrid = uniform_grid(dshape)

    reduced = c_dst.shape[0] + 2 == theta.shape[0]
    # 2) 求dx和dy.
    dx = TPS.z(ugrid.reshape((-1, 2)), c_dst, theta[:, 0]).reshape(dshape[:2])
    dy = TPS.z(ugrid.reshape((-1, 2)), c_dst, theta[:, 1]).reshape(dshape[:2])
    dgrid = np.stack((dx, dy), -1)

    grid = dgrid + ugrid
    
    return grid # H'xW'x2 grid[i,j] in range [0..1]

入力は 3 つのパラメーターです。

  • シータ reduced=True(N+2, 2) またはreduced=False(N+3, 2)
  • c_dst (N, 2) は、ターゲット テンプレート上のコントロール ポイントまたはランドマークです。
 c_dst = np.array([
    [0.693, 0.466],
    [0.808, 0.466],
    [0.572, 0.524],
    [0.923, 0.524],
    [0.545, 0.965],
    [0.954, 0.966],
])
  • dshape (H, W, 3) は、指定された参照イメージの解像度です。

出力は 1 です。

  • グリッド(H、W、2)。
    その視覚化を参照してください2.3.1

2.3.1uniform_grid

tps.tps_grid関数の最初のステップはugrid = unique_grid(dshape)です。この関数の定義は次のとおりです。その機能は 1 (H, W, 2) (H, W, 2)を作成することです。( H 2 )グリッド内の値はすべて 0 から 1 までの線形補間ですnp.linspace(0, 1, W(H))

def uniform_grid(shape):
    '''Uniform grid coordinates.
    '''

    H,W = shape[:2]    
    c = np.empty((H, W, 2))
    c[..., 0] = np.linspace(0, 1, W, dtype=np.float32)
    c[..., 1] = np.expand_dims(np.linspace(0, 1, H, dtype=np.float32), -1)

    return c

返される値は(H, W, 2) (H, W, 2)ugridです。( H 2 )グリッド、その X および Y 方向の値のサイズは、下図に示すように、方向に応じて線形に拡張されます。

X方向Y
ここに画像の説明を挿入します
方向
ここに画像の説明を挿入します

2.3.2和をTPS.z求めるために解くdxdy

 # 2) 求dx和dy.
 dx = TPS.z(ugrid.reshape((-1, 2)), c_dst, theta[:, 0]).reshape(dshape[:2]) # [H, W]
 dy = TPS.z(ugrid.reshape((-1, 2)), c_dst, theta[:, 1]).reshape(dshape[:2]) # [H, W]
 dgrid = np.stack((dx, dy), -1)  # [H, W, 2]

 grid = dgrid + ugrid

次の定義から、この関数がTPS.zX 方向と Y 方向を解決するスプライン関数であることが簡単にわかります。

f ( x / y ) ' ( x , y ) = a 1 + axx + ayy + ∑ i = 1 N wi U ( ∣ ∣ ( xi , yi ) − ( x , y ) ∣ ∣ ) f_{(x/y )^{'}}(x, y) = a_1 + a_x x + a_y y + \sum_{i=1}^{N} w_i U(|| (x_i, y_i) - (x, y) ||)f( x / y )( x ,y =ある1+ある×バツ+あるはいy+i = 1Nw私はU ( ∣∣ ( x私はy私は)( x ,y ) ∣∣ )

混乱を招く可能性があるのは、( )のときにパラメーターが同じである理由です。x は形状ですが、それでも です2.2TPS.d()cx(cy)(H*W), 2cc_dst (N,2)。私の理解では、2.3このステップの目標は真に画像面を位置に従って移動させることであるためです。制御点(曲げエネルギーを最小にする)を求めるため、ugrid平面上でサンプリングした点に対して一律にオフセット計算(dx和)を行うことでdy、導出条件を満たすオフセットの解析解を得ることができますdgrid

class TPS:
    ...
    @staticmethod
    def z(x, c, theta):
        x = np.atleast_2d(x)
        U = TPS.u(TPS.d(x, c)) # [H*W, N] 本例中H=W=800, N=6
        w, a = theta[:-3], theta[-3:]
        reduced = theta.shape[0] == c.shape[0] + 2
        if reduced:
            w = np.concatenate((-np.sum(w, keepdims=True), w))
        b = np.dot(U, w)
        return a[0] + a[1]*x[:, 0] + a[2]*x[:, 1] + b

ugrid+の場合、dgrid画像面全体のスプライン関数に従って計算されたdx、dy dx、dyが得られます。d x d y (オフセット) が均一の結果に追加されますX、 Y \mathbf{X}, \mathbf{Y}ugrid比較されていることがはっきりとわかります。2.3.1ugrid× Y方向にも対応する変化が生じます

X方向Y
ここに画像の説明を挿入します
ここに画像の説明を挿入します
方向
ここに画像の説明を挿入しますここに画像の説明を挿入します

この時点で、このステップで返されるのは実際にはX、Y \mathbf{X}, \mathbf{Y}2.3の間の値です。× 対応してY方向にねじれたグリッド (格子) (H、W、2) (H、W、2)( H 2 )、視覚化の結果は上記のとおりで、値の範囲は-1 から 1

2.4tps.tps_grid_to_remap

このステップは非常に簡単です。2.3計算された **グリッド**上でX、Y \mathbf{X}、\mathbf{Y}を押すだけです。× Y方向に対応するWWWHHH.次に、cv2.remap画像の歪み操作を実行する関数を送信します。

def warp_image_cv(img, c_src, c_dst, dshape=None):
    dshape = dshape or img.shape
    # 2.2
    theta = tps.tps_theta_from_points(c_src, c_dst, reduced=True)
    # 2.3
    grid = tps.tps_grid(theta, c_dst, dshape)
    # 2.4
    mapx, mapy = tps.tps_grid_to_remap(grid, img.shape)
    return cv2.remap(img, mapx, mapy, cv2.INTER_CUBIC)

2.4.1tps_grid_to_remapグリッドの幅と高さを単純に乗算する

def tps_grid_to_remap(grid, sshape):
    '''Convert a dense grid to OpenCV's remap compatible maps.
    Returns
    -------
    mapx : HxW array
    mapy : HxW array
    '''
    mx = (grid[:, :, 0] * sshape[1]).astype(np.float32)
    my = (grid[:, :, 1] * sshape[0]).astype(np.float32)

    return mx, my

ここに画像の説明を挿入します

2.4.2cv2.remap(img, mapx, mapy, cv2.INTER_CUBIC)ワープ後の結果を取得します。

cv2.remapユーザーが独自のマッピング関係を定義できる機能です。アフィン変換変換行列による透視変換とは異なり、より柔軟なTPSマッピングです。具体例を参照してください[12]

ここに画像の説明を挿入します

なお、この結果が「まえがき」の結果と異なるのは、「まえがき」ではマスキングにマスクを使用したためです。

要約する

この時点で、TPS分析は終了します。このアルゴリズムは、顔の痩身やテクスチャ マッピングなどのタスクで最も一般的です。また、非常に柔軟なワーピング アルゴリズムでもあります。今でも広く使用されています。この記事に関しては、お気軽にコメントしてください。下記にご指摘ください、
ありがとうございます^ . ^

参考文献

  1. 薄板スプライン: MathWorld
  2. 二調和方程式: MathWorld
  3. c0ldHEart: 薄板スプライン TPS 薄板スプライン変換の基本的な理解
  4. WITH: ワープモーフ
  5. 薄板スプラインマッピングと主反りの近似法
  6. cheind/py-薄板スプライン
  7. 薄板-スプライン-反り
  8. Wikipedia: 薄板スプライン
  9. Deep Shallownet: 放射基底関数カーネル - ガウス カーネル
  10. ブックスタイン: 反りの原理: 薄板スプラインと変形の分解
  11. Zhihu: 「機能的」とは正確にはどういう意味ですか?
  12. [opencv] 5.5 幾何学的変換 -- 再マッピング cv2.remap()

おすすめ

転載: blog.csdn.net/g11d111/article/details/128641313