TouchDesignerで射影変換
TouchDesignerでOpenCVを使用して射影変換を行います。
射影変換とは
射影変換は平面上の任意の4点を別の4点に対応付ける変換で、変換は3x3の変換行列として表現されます。
今回の検証には本を斜めから撮影した画像を変換元、本のカバー写真を変換先にしており、本の4隅の位置を元に変換行列を求めます。
変換元の画像はサイズが(1280, 960)
、左下を原点に本の左上隅から時計回りに4点が(624, 841), (992, 695), (634, 242), (218, 518)
となります。同様に変換先の画像はサイズが(1280, 1280)
、4点が(382, 968), (898, 968), (898, 311), (382, 311)
になります。
Script TOPで射影変換
Script TOPで射影変換を行います。射影変換を行うには、あらかじめ求めておいた4点の位置からOpenCVのgetPerspectiveTransform
メソッドで変換行列を求めて、warpPerspective
で入力画像を変換します。最後にComposite TOPで変換結果を変換元の画像と重ねて正しく変換されているかを確認しています。
import cv2
import numpy as np
def onCook(scriptOp):
srcFrame = scriptOp.inputs[0].numpyArray()
srcPoints = np.float32([(624, 841), (992, 695), (634, 242), (218, 518)])
dstPoints = np.float32([(382, 968), (898, 968), (898, 311), (382, 311)])
# 変換行列を求める
M = cv2.getPerspectiveTransform(srcPoints, dstPoints)
# 入力画像を射影変換する、(1280, 1280)は出力画像のサイズ
dstFrame= cv2.warpPerspective(srcFrame, M, (1280, 1280))
scriptOp.copyNumpyArray(dstFrame)
変換行列のキャッシュ
ここまでで射影変換を行うことができましたが、変換元と変換先の4点が変わらない場合には一度だけ変換行列を求めればよいので、変換行列をキャッシュして無駄な計算を削減します。
以下の例ではText DATに変換行列を求めるスクリプトがあり、これをRun Script
すると求めた変換行列をTable DATに書き込みます。Script TOPでは変換行列を求めずにTable DATから変換行列の値を取得するようにしています。
import cv2
import numpy as np
srcPoints = np.float32([(624, 841), (992, 695), (634, 242), (218, 518)])
dstPoints = np.float32([(382, 968), (898, 968), (898, 311), (382, 311)])
# 変換行列を求めてTable DATに書き込み
M = cv2.getPerspectiveTransform(srcPoints, dstPoints)
op('table1').clear()
op('table1').appendRow(M.flatten())
import cv2
import numpy as np
def onCook(scriptOp):
srcFrame = scriptOp.inputs[0].numpyArray()
# Table DATから変換行列を読み込み
M = np.float32([cell.val for cell in op('table1').row(0)]).reshape(3, 3)
dstFrame= cv2.warpPerspective(srcFrame, M, (1280, 1280))
scriptOp.copyNumpyArray(dstFrame)
GLSL TOPで射影変換
ここまではScript TOPを用いてきましたが、GLSL TOPを用いてGPUで変換したほうがパフォーマンス的に良いケースもあります。
GLSL TOPでは変換先の座標から変換元の座標を計算して変換元の画像をサンプルします。そのため、getPerspectiveTransform
で変換行列を求める際には引数を逆にして変換先から変換元への変換行列を計算する必要があります。
GLSL TOPのパラメータダイアログのVectors
タブで変換元画像のサイズ(u_srcSize
)、変換先画像のサイズ(u_dstSize
)を、Arrays
タブで変換行列(u_matrix
)をUniform値として設定します。変換行列は行列なのでMatrices
タブで設定できそうですが、Matrices
タブでは4x4行列しか設定できず、射影変換の変換行列は3x3行列なので使用できません。
import cv2
import numpy as np
srcPoints = np.float32([(624, 841), (992, 695), (634, 242), (218, 518)])
dstPoints = np.float32([(382, 968), (898, 968), (898, 311), (382, 311)])
# 引数の順序が逆になっていることに注意
M = cv2.getPerspectiveTransform(dstPoints, srcPoints)
op('table1').clear()
op('table1').appendRow(M.flatten())
uniform vec2 u_srcSize;
uniform vec2 u_dstSize;
uniform float[] u_matrix;
out vec4 fragColor;
void main()
{
vec2 uv = vUV.xy;
mat3 M = mat3(
u_matrix[0], u_matrix[3], u_matrix[6],
u_matrix[1], u_matrix[4], u_matrix[7],
u_matrix[2], u_matrix[5], u_matrix[8]
);
vec2 dstPos = uv * u_dstSize;
vec3 transPos = M * vec3(dstPos, 1.0);
vec2 srcPos = transPos.xy / transPos.z;
vec2 srcUv = srcPos / u_srcSize;
fragColor = TDOutputSwizzle(texture(sTD2DInputs[0], srcUv));
}
サンプル
ここまでは斜めに撮影した画像を上から俯瞰した画像へ変換していましたが、変換元と変換先を入れ替えれば俯瞰した画像から斜めに撮影した画像にも当然変換することができます。
以下の例では本の表紙に合わせて別の画像を重ねていますが、斜めに撮影した画像に2次元的にパースを合わせてコンテンツを重ねることができます。
Discussion