【AR】マッチムーブを実装してみた【Python】


マッチムーブ

ってなにやねんって話ですよね
結果をみてみましょう

元動画(qiitaにあげるためにgif化)

変換後(qiitaにあげるためにgif化)

ちょっとgif化して容量を落とす際に範囲やサイズがちょっと違うものになってしまったんですが
雑誌中(画質悪すぎて読めませんね!)の写真の部分を別の写真に置き換えています。

このように動画の狙っている部分のみ(ターゲット領域)を別の画像に置き換えることがマッチムーブです。
ターゲット領域は動いたり、カメラの移動により斜めから撮ることで変形に対応した出力を用意します。

実はこの技術自体はあまり新しいものではなく、また、CNNなどを用いた技術でもありませんので計算機も必要ありません。

実装の過程

このマッチムーブですが、今回は以下の3ステップで行います。

①特徴点の検出
②ホモグラフィー行列の導出
③変換後画像の生成

次に事前に準備が必要な画像について説明します。
a.元画像および元動画(変換を適用する背景にもなる)
b.ターゲット領域(aから狙ってきたいところをスクショとってくれば良い。座標を指定してもいいんだけど、座標与えるよりスクショでいいやと思った)
c.置き換える用画像(大体狙ってるターゲット領域を似た形の画像がいい。基本何でもいい)

d.環境
--opencv, numpyくらい?

①特徴点抽出

ここはあまり本題じゃないんですが、特徴点というのは、画像の中の特徴がありそうな点を座標と特徴量を抽出します。
あんまりよくわかってませんが、よくできているものです。

get_match_features関数
def get_match_features(src, target):
    akaze = cv2.AKAZE_create()
    kp1, des1 = akaze.detectAndCompute(src, None)
    kp2, des2 = akaze.detectAndCompute(target, None)

    bf = cv2.BFMatcher(cv2.NORM_HAMMING, crossCheck=True)

    matches = bf.match(des1, des2)

    matches = sorted(matches, key=lambda x: x.distance)
    good_match_rate = 0.5 
    good = matches[:int(len(matches) * good_match_rate)]
    src_pts = np.float32(
            [kp1[m.queryIdx].pt for m in good]).reshape(-1, 1, 2)
    dst_pts = np.float32(
           [kp2[m.trainIdx].pt for m in good]).reshape(-1, 1, 2)

    cv2.imwrite('draw_match.jpg', cv2.drawMatches(src, kp1, target, kp2, matches[:10], None, flags=2))
    return src_pts, dst_pts

この関数によりこんな感じで対応点が得られます。

src_pts, dst_ptsには、特徴点の個数×2(x,y)が返されます。
回転してようがなんだろうが、基本的にあんまり関係ありません。
のちのホモグラフィー変換が回転などの変形に対応しているからです。

②ホモグラフィー行列の導出

対応点が得られると、対応点が一致するように射影変換をすることができます。
有名なのは、アフィン変換があります。アフィン変換の場合は、拡大縮小や、せん断(長方形を平行四辺形にする感じ)に対応できます。
ホモグラフィー行列による変形の場合は、アフィン変換に加え、台形にも対応できます。カメラの角度が変わったり、距離が変わると対象の写真が台形になることがあります。この求めた対応点からホモグラフィー行列を求めることで台形に変形させます。

対応点からホモグラフィー行列を求める関数と

match_move.py
def get_homography(src_pts, dst_pts):
    h, mask = cv2.findHomography(src_pts, dst_pts, cv2.RANSAC)
    return h

求めたホモグラフィー行列をもとにホモグフラフィー変換を行う関数です。

match_move.py
def transform_homography(ad_img, h, im2):
    ad_img = cv2.warpPerspective(ad_img, h, (im2.shape[1],im2.shape[0]))
    return ad_img

ホモグラフィー行列ですが、
①get_homographt(src_pts, dst_pts)

②get_homographt(dst_pts, src_pts)

とするかで意味が変わります。

src_ptsが雑誌から抽出された特徴点(目印で白い点を打ち込んである)

dst_ptsが雑誌中の写真(こいつを雑誌中から探索する。多分正面から撮影が好ましい)から抽出された特徴点とすると、

①get_homographt(src_pts, dst_pts)
の場合は、雑誌の画像を、写真の領域のように変形するホモグラフィー行列が得られます。

元々雑誌中にあった白い点があることから、雑誌のものが変形されて、この写真が得られていることがわかります。
ホモグラフィー変換について調べるとこっちの出力が多いです。

画像中の斜めになった看板を長方形に戻すなどそう言ったことをすることができます。

一方で
②get_homographt(dst_pts, src_pts)
の場合は、何が出力されるか。みた方が早いです。こうなります

なるほどって思いましたかね。
要するに、ARの置き換えたい部分だけの画像ができます。

これを違う画像から同じホモグラフィー変換を行うとこんな感じのことができちゃうわけですね。

なんかできそうな雰囲気できてきましたね。(RGBとBGR間違って少しバグったやつだけどなんか良さげだからそのままにしてある)

解説

cv2.warpPerspective(ad_img, h, (im2.shape[1],im2.shape[0]))
実際にホモグラフィー変換をしているこのwarpPerspective関数ですが、
ホモグラフィー変換は平行移動を持っています。なので、「画素がある範囲が画像だ」みたいにすると、行列次第では無限サイズの画像になってしまう可能性があります。そこで、画像サイズを決める必要があるんですよね。そこが第3引数で渡しているところです。

①の方では、本当は出力画像の周りにも画素はあるんですが、第3引数の範囲外のものは表示されないので、あたかも切り取られているかのような出力になってます。

②の方では、逆に写真を雑誌サイズに変更する引数を渡しているものの、周りには画素がないので、黒いもので塗り潰されています。

③変換後画像の生成

生成された画像の黒い部分は、本当の真っ黒=((0,0,0))です。
一般に写真などではこの真っ黒を置き換えちゃえばいいです。
良い関数があればそれでも良かったですが、面倒なんので普通に置き換えちゃいました。

match_move.py
def fusion(src, generated):
    height,width,c = src.shape
    src = src.reshape(-1)
    generated = generated.reshape(-1)

    generated[generated==0] = src[generated==0]
    return generated.reshape(height,width,c)

こんな感じで狙ってた画像が、どこでどんな形をしているかを突き止めて、その上で変形して上書きすることができます。
あとはこれを連続でやれば動画です。

動画化

いろいろ省いていますが、上記のマッチムーブを毎フレームでやればいいだけです。マッチムーブして生成した画像をくっつけて動画をつくっているだけです。

動画化のコード
video_process.py
def main():
    #video名前など取得
    args = load_args()

    #videoのロード
    video = load_video(args.video_name)
    # 幅
    W = video.get(cv2.CAP_PROP_FRAME_WIDTH)
    # 高さ
    H = video.get(cv2.CAP_PROP_FRAME_HEIGHT)

    fourcc = cv2.VideoWriter_fourcc(*'mp4v')
    # fourcc = cv2.VideoWriter_fourcc(*'XVID')

    out = cv2.VideoWriter(
            'output/'+args.video_name,
            fourcc,
            30, 
            (int(W),int(H))
            )   

    t = 0 
    psnr_th = 4 
    while True:
       ret, frame = video.read()
       if ret==False:
           break
       output_frame,psnr = match_move.video_function(base = frame)
       print(t, psnr)

       if True:
           cv2.imwrite("tmp/tmp"+str(t)+".jpg", output_frame)
           t += 1
       # match_move.video_function(base = frame, args.box_name, args.ad_name)
       if psnr > psnr_th:
           print("fixed")
           output_frame = previous_frame
       else:
           previous_frame = output_frame
       out.write(output_frame) 

    cv2.destroyAllWindows()
    out.release()
    video.release()

ところで、動画の場合はちょっとだけ工夫をすることができます。
動画では結構動いた時のブレた画像があります。1フレーム程度だと人間は視認できないですが、マッチムーブでは、シンプルに特徴点マッチングからうまくいかないため、カオスな画像が返されてしまいます。

そこで、ホモグラフィー変換の結果、ターゲット領域とほぼ同じ画像の場合は、OKで、逆に全然違う画像(=特徴点がうまくマッチしていない場合)は1つ前の画像をそのまま用いると言う方法で、崩壊が発生したフレームを動画化の際に取り除くことができます。
画像間の類似度はcv2.psnr()で計測できます。
それにより比較的安定したフレームのみで出力の動画を作ることができます。

感想

普段CNNなどを用いて何かすることは多いですけど、CNN以前の技術も結構面白いことできそうだなーとかなり感じました!
簡単なものなら物体検出くらいならそれなりにできそうです。

github

便利だった

gif maker
https://ezgif.com/resize