Affine行列の線形結合により、2点の変換の中間状態を作り出す事で簡単な動きを持たせたアニメーションを作ってみます。ちょっとしたお遊びですが何かに応用できそうです。Python + OpenCVを使って作ってみます。最後にコードも載せておきます。

Affine行列の特徴
Affine変換自体は座標変換する際に、回転、移動、拡大・縮小、(厳密にはせん断も)の変換をすることができます。一般的には3×2の行列で表されます。とても便利で行列の掛け算でいくらでも変換が重ね合わせられます。
この記事で射影変換で試していますが、Affine同士も同じです。
もう一つ便利なのは、Aという変換とBという変換の中間CはC=(A+B)/2で表現できてしまいます。例えばAが1倍の変換、Bが3倍の拡大であれば、Cは2倍の拡大の行列になります。移動もしかりです。
これを応用して、簡単なアニメーションを作ってみます。
Affine行列の補間
ある地点Aから移動先のある地点Bの3点の座標対さえあればその変換は簡単に求められます。
またその次の地点Cへの座標対も求めます。AからB、AからCの2つの変換行列を求めればその間の変化はBとCの行列の線形補間で求めることができます。これを繰り返すことで簡単なアニメーションを作ります。
まず適当な背景を用意して、そこにまた適当なオブジェクトを置きます。今回のオブジェクトはそこらで撮った小鳥です。

詳しい説明は省きますが、3点の座標対があればAffine行列は確定します。しかもOpenCVであれば面倒な逆行列の演算もせずに、関数一発でAffine行列を解いてくれます。
今回は左上の座標、右上の座標、左下の座標の角3点の移動軌跡をとっていきます。

まずはスタート地点。鳥を原点から、画像の中央下まで移動させます。0,0の点から368,368の点まで移動させます。幅、高さの変更はしないので、角3点はそれぞれ、
pts1 = np.float32([[0,0],[w,0],[0,h]])
pts2 = np.float32([[368,368],[368+w,368],[368,368+h]])
となります。ここからpts1からpts2への変換行列を求めます。
M1 = cv2.getAffineTransform(pts1,pts2)
で終わりです。

次に右(658,180)へ移動させます。同じように、
pts3 = np.float32([[658,180],[658+w,180],[658,180+h]])
こんな感じで座標を取ります。同じように鳥のオブジェクトを原点からpts3へ変換するので、その行列は
M2 = cv2.getAffineTransform(pts1,pts3)
となります。これで中央下への変換行列M1と右への変換行列M2が求まりました。この2点を線形補間すると間の状態ができます。
for i in range(frame):
M3 = (M2 * i + M1 * (frame - i)) / frame
canvas = back.copy()
cv2.warpAffine(img, M3, (ww, hh), borderMode=cv2.BORDER_TRANSPARENT, dst=canvas)
こんな感じで中間状態M3の変換行列を求めてアフィン変換します。アフィン変換自体はwarpAffine関数を使います。この時のborderModeはcv2.BORDER_TRANSPARENTを使ってあげます。こうすることで、dst=で指定した画像が後ろに透けます。このforループではframe回だけ中間状態を作っています。
ちなみにこのModeだと、引数の画像を上書くので、渡す前に画像のdeep copyをとっておいてから渡さないと、画像がどんどん上書きされます。ここでは背景となる画像backをいったんcanvasにコピーしてからAffine変換しています。

まずはこんな感じで移動していきます。
次に上へ回転を伴って移動させます。

同じように座標を取ります。こればかりは手動です。
M1 = M2
pts3 = np.float32([[483,22],[483,22+w],[483-h,22]])
M2 = cv2.getAffineTransform(pts1,pts3)
前回作ったM2をスタートにするためにいったんM1へコピーします。次の座標をpts3へ代入し、Affine行列を解いてM2を作ります。これで同様に上へ回転しながらオブジェクトが移動します。

途中縮小されるように見えます。どうしても線形結合なので、行列中のScaleの成分も変動します。拡大・縮小を抑制しようと思ったら、求まった行列式からScale成分を取り出して、それを1倍にした行列にすれば直ると思います。回転成分だけにすればよいと思います。試してないですけど。
同じように左へ、今度は回転と縮小を伴って。

そして下へ、回転と反転を加えます。

すると出来上がるアニメーションとしてはこんな感じになります。

移動の軌跡はこんな感じ。

まぁ思った通りです。
比較的簡単な記述でちょっとしたアニメーションができました。αブレンド入れたり、複数のコマを入れ替わりで差し込んだりすればもう少し凝ったアニメーションも簡単に作れそうです。座標を取るのがめんどくさかった。
コード
座標は直値です…。
def interpolate(img, back, M1, M2, frame, j):
for i in range(frame):
j = j+1
M3 = (M2 * i + M1 * (frame - i)) / frame
canvas = back.copy()
cv2.warpAffine(img, M3, (ww, hh), borderMode=cv2.BORDER_TRANSPARENT, dst=canvas)
fname = "{0:03d}".format(j) + ".jpg"
h, w = canvas.shape[:2]
size = (w // 2, h // 2)
canvas = cv2.resize(canvas, size)
cv2.imwrite(fname, canvas)
img = cv2.imread("back.jpg")
img2 = cv2.imread("bird.jpg")
hh, ww = img.shape[:2]
h, w = img2.shape[:2]
frame = 15
pts1 = np.float32([[0,0],[w,0],[0,h]])
pts2 = np.float32([[368,368],[368+w,368],[368,368+h]])
pts3 = np.float32([[658,180],[658+w,180],[658,180+h]])
M1 = cv2.getAffineTransform(pts1,pts2)
M2 = cv2.getAffineTransform(pts1,pts3)
interpolate(img2, img, M1, M2, frame,0)
M1 = M2
pts3 = np.float32([[483,22],[483,22+w],[483-h,22]])
M2 = cv2.getAffineTransform(pts1,pts3)
interpolate(img2, img, M1, M2, frame,frame)
M1 = M2
pts3 = np.float32([[305,318],[305-80,318],[305,318-80]])
M2 = cv2.getAffineTransform(pts1,pts3)
interpolate(img2, img, M1, M2, frame,frame*2)
M1 = M2
pts3 = np.float32([[368,496],[368+w,496],[368,496-h]])
M2 = cv2.getAffineTransform(pts1,pts3)
interpolate(img2, img, M1, M2, frame,frame*3)
コメント