OpenCV
python3
GoPro

定点撮りした動画から、フレームインした箇所だけ動画を抜き出す

10分とか撮りっぱなしにした動画をすべて見るのがめんどくさいので、人がフレームインしてからフレームアウトするまでを抜き出したい。

環境

Windows10 Home
Python 3.6.5
open cv 3.4.2

PS C:\work\cutMovie> C:\Python36\python.exe --version
Python 3.6.5
PS C:\work\cutMovie> C:\Python36\python.exe
Python 3.6.5 (v3.6.5:f59c0932b4, Mar 28 2018, 16:07:46) [MSC v.1900 32 bit (Intel)] on win32
Type "help", "copyright", "credits" or "license" for more information.
>>> import cv2
>>> cv2.__version__
'3.4.1'
>>> exit()
PS C:\work\cutMovie>

概要

フレームをグレースケール化したものの移動平均を cv2.accumulateWeighted で取得し、移動平均との差分を cv2.absdiff で取得しています。
さらに cv2.threshold で二値化したものに対して cv2.norm を実行することでノルム化しています。
ノルム化した値の移動平均との乖離を利用して動画の開始、終了位置を指定しています。
この際、1フレームだけの乖離値で判定すると外れ値を拾ってしまうため、直近3フレームすべてが指定した乖離値(ここでは1.3)を超えた場合に開始と判定しています。
また、開始位置からだと前後の関係が全く分からなくなってしまうため、120フレーム分さかのぼって出力するようにしています。ここで使用した動画は60fpsなので時間にしたら約2秒です。30fpsであれば4秒となります。
また、最低300フレームは動画を終了しないようにしています。
エラーチェックなどは行っていません。

ソースコード

cutMovie.py
#-*- coding:utf-8 -*-
import cv2
import numpy as np
import sys

FILENAME=sys.argv[1].split(".")[0]
FILEEXT="."+sys.argv[1].split(".")[1]
FPS=60

MIN_LENGTH=300 #最低フレーム数
PRE_FRAME_LENGTH=120 #開始位置より前に入れるフレーム数
NUM_NORM_AVG=60 #normの移動平均に使うフレーム数
START_WEIGHT = 1.2 #終了判定に利用する重み
END_WEIGHT = 0.7 #終了判定に利用する重み

def main():

    # 動画の読み込み
    print(FILENAME+FILEEXT)
    cap = cv2.VideoCapture(FILENAME+FILEEXT)
    # フレームを取得
    ret, frame = cap.read()
    height, width, channels = frame.shape
    fourcc = cv2.VideoWriter_fourcc(*'MJPG')
    out = cv2.VideoWriter()  # 'False' for 1-ch instead of 3-ch for color
    #print(out.isOpened())

    prev = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY)
    #print(cv2.norm(prev))
    avg = prev.copy().astype("float")
    norms = []
    frames = []
    st=0
    cnt = 0
    fCnt = 0
    # 動画終了まで繰り返し
    while(ret):
        fGray = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY)
        cv2.accumulateWeighted(fGray, avg, 0.5)
        frameDelta = cv2.absdiff(fGray, cv2.convertScaleAbs(avg))
        thresh = cv2.threshold(frameDelta, 3, 255, cv2.THRESH_BINARY)[1]
        value = cv2.norm(thresh)
        norms.append(value)
        normAvg = sum(norms) / len(norms)
        #print(value, normAvg)
        if (value > normAvg * START_WEIGHT and norms[-2] > normAvg * START_WEIGHT and norms[-3] > normAvg * START_WEIGHT) or st == 1:
          if st == 0:
            out.open('./'+FILENAME+'_'+str(cnt)+'.avi',fourcc, FPS, (width, height), True)
            st = 1
            for f in frames:
              out.write(f)
          fCnt = fCnt + 1
          out.write(frame)
        #print(value, normAvg, st, fCnt)
        if (value < normAvg * END_WEIGHT and norms[-2] < normAvg * END_WEIGHT and norms[-3] < normAvg * END_WEIGHT) and st == 1 and fCnt > MIN_LENGTH:
          print(cnt)
          st = 0
          cnt=cnt+1
          fCnt = 0
          out.release()
          frames = []
        if len(norms) > NUM_NORM_AVG:
          norms.pop(0)
        if st == 0:
          frames.append(frame)
        if len(frames) > PRE_FRAME_LENGTH:
          frames.pop(0)

        ret, frame = cap.read()
        #prev = fGray

    cap.release()
    out.release()
    cv2.destroyAllWindows()


if __name__ == "__main__":
    main()

使い方

C:\work\cutMovie\GOPR3040.MP4 を分割したい場合
pythonについてはパスとか通してあればフルパスで書く必要はありません。

PS C:\work\cutMovie> C:\Python36\python.exe C:\work\cutMovie\cutMovie.py C:\work\cutMovie\GOPR3040.MP4

参考
https://developers.cyberagent.co.jp/blog/archives/12666/