サイトロゴ

【Python】VidStabで手ぶれ補正【動画編集への道#2】

著者画像
Toshihiko Arai

はじめに

Pythonやシェルを駆使して動画編集を行なっていく企画の「第二弾」になります。前回の記事と合わせてご覧ください。

この記事では、Pythonで動画の手ぶれ補正をするライブラリ「VidStab」の使い方を中心に解説していきます。

VidStab(Python Video Stabilization)

VidStabモジュールは、コアにOpenCVを使用した動画の手振れを修正できるプログラムになります。OpenCVへの理解がなくても、VidStabを利用すれば手っ取り早く手ぶれ補正ができますのでご安心ください。

vidstab と OpenCV のインストール

Python3.xで動作させていきます。vidstabをpipでインストールします。

$ pip install vidstab

vidstabを動かすにはOpenCVも必要になりますので合わせてインストールしてください。

$ pip install opencv-python
$ pip install opencv-contrib-python

VidStab で手ぶれ補正する

それではさっそく、VidStab で手ぶれ補正してみましょう。自転車に乗りながらスマホをチェストマウントして撮影した動画になります。

元動画(左)ではブレブレだった映像も、手ぶれ補正後はだいぶマシになったように感じます。

プログラムは次のとおりです。たったの数行で手ぶれ補正ができちゃいます。

from vidstab import VidStab


stabilizer = VidStab(kp_method='ORB')
stabilizer.stabilize(input_path='../build/in.mp4', output_path='../build/out_orb.avi')

VidStabを使えば、驚くおほど簡単に手ぶれ補正した動画が作れちゃいますね!ただし注意点としまして、avi 形式で保存しないと機能しませんでした。

黒いボーダーを reflect(反射)で埋める

手ぶれ補正すると、端の部分に黒い隙間ができてしまいます。黒い部分を含めないように MoviePyなどでCrop処理 しても良いのですが、VidStabのreflect(反射)機能を使うことで目立たなくさせることもできます。

from moviepy.editor import *
from moviepy.video.tools.segmenting import findObjects
import moviepy.video.fx.all as vfx
import os


def crop(clip, region, margin = 50):
    w,h = clip.size

    clip = clip.set_mask(region.mask).set_pos(region.screenpos)
    
    clip = vfx.crop(clip,
        x1=(margin), width=(w - margin * 2),
        y1=(margin), height=(h - margin * 2) 
    )
    return clip.resize(region.size)
    

if __name__ == "__main__":

    im = ImageClip("../assets/splitter_800x900.png")

    # Loacate the regions, return a list of ImageClips
    regions = findObjects(im) # region ( リージョン): 範囲、領域、分野、(身体の)部位の意味

    print(len(regions)) # ←これが0なら、マスク画像の作成で何か間違ってる。(黒枠でちゃんと囲えてないとか、透過だとダメで白黒でやる)

    clip1 = crop(VideoFileClip("../build/out_orb.avi", audio=False), regions[0])
    clip2 = crop(VideoFileClip("../build/out_orb_reflect.avi", audio=False), regions[1])

    final_clip = CompositeVideoClip([clip1, clip2], im.size).subclip(0.5, clip2.duration - 0.5)

    out_path = "../build/pip_orb_reflect.mp4"
    final_clip.write_videofile(out_path)

    os.system('open ' + out_path)

他にもkp_method には replicate(複製)が指定できますので各自で試してみてください。

あとは、playback=True を指定すると、動画の書き出しを待たずにその場でプレビューできて便利です。

stabilizer.stabilize(input_path='../build/in.mp4', output_path='../build/out.avi', border_type='replicate', playback=True)

手ぶれ補正に向いてない動画もある

こちらの動画のように、元動画よりもガタガタになったり、追従が追いつかない場合もあります。こういった場合は、無理にVidStabを使う必要もないのかなと。または、LPFの強度を調整する方法を学ぶ必要がありますかね。

VidStabはC++で書かれたソース 「SIMPLE VIDEO STABILIZATION USING OPENCV」 をもとに作られているようです。 内容からするに、アルゴリズム的にはOpenCVでコーナーを抽出して、角度を算出、前後のフレームと比較して移動平均のフィルタ(LPF)をかけているようです。

移動平均のフィルタは結構単純でして、 でも使用しました。

移動平均などのローパスフィルタは、簡単にプログラミングできてそれなりに有効というのがメリットです。しかし、フィルタを強くかけすぎると動きの速さに追従できなくなり、先ほどの動画のようにカメラの向きを変えたときに遅延が生じます。逆にフィルタを弱めれば、手ぶれ補正の役割を果たせなくなります。 そういった中で、さらに優秀なフィルタとしてカルマンフィルタが挙げられます。先ほどの 「SIMPLE VIDEO STABILIZATION USING OPENCV」 のサイトにも、カルマンフィルタバージョンの手ぶれ補正も公開されてます。また別の機会に、試した結果をご紹介します。

numpyやOpenCVでフレームを処理する

VidStabでは他にもこんなことができます。手ぶれ補正した動画フレームに、その場でnumpyやOpenCVで処理させることができます。

チュートリアル にありましたサンプル例を分かりやすく書き直したものをご紹介しましょう。

OpenCVでオブジェクトを追跡する

このプログラムは、OpenCVのプレビューウィンドウが起動し、追跡したいオブジェクトをマウスでドラッグして選択後、Enterする必要があります。VidStabとOpenCVの処理がごっちゃになって書かれているので分かりずらいですが、コメントアウトを頼りに解読してみてください。

from ast import IsNot
import os
import cv2
from vidstab import VidStab, layer_overlay, download_ostrich_video


object_tracker = cv2.TrackerCSRT_create()
stabilizer = VidStab(kp_method='ORB')
vidcap = cv2.VideoCapture("./build/in.mp4")

fmt = cv2.VideoWriter_fourcc('m', 'p', '4', 'v') # ファイル形式(ここではmp4)
writer = cv2.VideoWriter('./build/stab_object_tracking.mp4', fmt, 30, (1280, 720)) # ライター作成


object_bounding_box = None # オブジェクトを四角で囲む
counter = 0

while True:
    grabbed_frame, frame = vidcap.read()

    stabilized_frame = stabilizer.stabilize_frame(input_frame=frame, border_size=50)

    # If stabilized_frame is None then there are no frames left to process
    if stabilized_frame is None:
        print('stabilized_frame is None')
        break
    

    # Draw rectangle around tracked object if tracking has started
    if object_bounding_box is not None:
        success, object_bounding_box = object_tracker.update(stabilized_frame)

        if success:
            (x, y, w, h) = [int(v) for v in object_bounding_box]
            cv2.rectangle(stabilized_frame, (x, y), (x + w, y + h),
                          (0, 255, 0), 2)

    cv2.imshow('Frame', stabilized_frame) # プレビューウィンドの表示
    stabilized_frame = cv2.resize(stabilized_frame,(1280, 720)) # リサイズしないと動画が正しく書き出せない
    writer.write(stabilized_frame) 

    # cv2.imwrite("./build/tmp/{}.jpg".format(counter), stabilized_frame)
    # counter += 1

    key = cv2.waitKey(5)

    if stabilized_frame.sum() > 0 and object_bounding_box is None:
        # 追跡したいオブジェクトをマウスでドラッグして選択後、Enterする
        object_bounding_box = cv2.selectROI("Frame",
                                            stabilized_frame,
                                            fromCenter=False,
                                            showCrosshair=True)

        object_tracker.init(stabilized_frame, object_bounding_box)

    elif key == 27:
        print('key == 27')
        break

writer.release() 
vidcap.release()
cv2.destroyAllWindows()
print('released')

オブジェクトを四角で囲む処理はあくまでもOpenCVの機能です。これをさらに発展させれば、ターゲットを映像の中心に持ってきたり、人の顔などをモザイク処理できたりと、いろいろな場面で応用ができそうです。

OpenCVで追跡したオブジェクトに画像を重ねる

ターゲットにしたオブジェクトの位置に透過画像を重ねることもできます。

from ast import IsNot
import os
import cv2
from vidstab import VidStab, layer_overlay, download_ostrich_video


object_tracker = cv2.TrackerCSRT_create()
stabilizer = VidStab(kp_method='ORB')
vidcap = cv2.VideoCapture("./build/in.mp4")

fmt = cv2.VideoWriter_fourcc('m', 'p', '4', 'v')
writer = cv2.VideoWriter('../build/stab_object_tracking_replace.mp4', fmt, 30, (1280, 720))

object_bounding_box = None # オブジェクトを四角で囲む
mask_img = cv2.imread("../assets/apple.png", cv2.IMREAD_UNCHANGED) # アルファチャンネル込みで読み込む

while True:
    grabbed_frame, frame = vidcap.read()

    stabilized_frame = stabilizer.stabilize_frame(input_frame=frame, border_size=50, border_type='reflect')


    if stabilized_frame is None:
        print('stabilized_frame is None')
        break
    

    if object_bounding_box is not None:
        success, object_bounding_box = object_tracker.update(stabilized_frame)

        if success:
            (x, y, w, h) = [int(v) for v in object_bounding_box]
            #cv2.rectangle(stabilized_frame, (x, y), (x + w, y + h), (0, 255, 0), 2)
            #stabilized_frame = cv2.addWeighted(src1=stabilized_frame, alpha=1.0, src2=mask_img, beta=1.0, gamma=0)
            mask_img = cv2.resize(mask_img, (100, 100))
            #stabilized_frame[y:y+100, x:x+100] = mask_img[:, :, :3]
            stabilized_frame[y:y+100, x:x+100] = stabilized_frame[y:y+100, x:x+100] * (1 - mask_img[:, :, 3:] / 255) + mask_img[:, :, :3] * (mask_img[:, :, 3:] / 255)

    cv2.imshow('Frame', stabilized_frame) # プレビューウィンドの表示
    stabilized_frame = cv2.resize(stabilized_frame,(1280, 720)) # リサイズしないと動画が正しく書き出せない
    writer.write(stabilized_frame) 


    key = cv2.waitKey(5)

    if stabilized_frame.sum() > 0 and object_bounding_box is None:
        # 追跡したいオブジェクトをマウスでドラッグして選択後、Enterする
        object_bounding_box = cv2.selectROI("Frame",
                                            stabilized_frame,
                                            fromCenter=False,
                                            showCrosshair=True)

        object_tracker.init(stabilized_frame, object_bounding_box)

    elif key == 27:
        print('key == 27')
        break

writer.release() 
vidcap.release()
cv2.destroyAllWindows()
print('released')

関連記事