Zenn
🖼️

TouchDesignerで射影変換

2025/03/19に公開

TouchDesignerでOpenCVを使用して射影変換を行います。

射影変換とは

射影変換は平面上の任意の4点を別の4点に対応付ける変換で、変換は3x3の変換行列として表現されます。

https://qiita.com/ydclab_0003/items/1fc3d1468a39623bae60

今回の検証には本を斜めから撮影した画像を変換元、本のカバー写真を変換先にしており、本の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で変換結果を変換元の画像と重ねて正しく変換されているかを確認しています。

script1_callbacks
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から変換行列の値を取得するようにしています。

text1
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())
script1_callbacks
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行列なので使用できません。

text1
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())
glsl_pixel
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

ログインするとコメントできます