動画ファイルのスロー再生

録画した動画をスロー再生したくて、スロー再生用のスクリプトを作成しました。
フレーム間に待ち時間を入れることで、スロー再生しているように見せかけているだけです。したがって、カクカクした再生になりますが、それなりに再生できているようです。
自身のゴルフスイングを見るために使用しています。もう少し欲しい瞬間があるのですが、おおむね目的ははたされています(スイングのチェック)。iphoneを利用して撮影した動画なので、fpsが約30フレーム/秒です。もう少し高速に録画できる機材で録画できれば、もっと楽しい瞬間を確認できるのにとも思います。
いつか、フレーム間の画像を補完生成し動画を作成することに挑戦してみようと思います。

仕様

  • スロー再生用のクラス  Slow(file_path,c_time)
       file_path : 再生するファイルの名前  必須
             カレントディレクトリ以外は、パスを付けること
       c_time :    チャプターを設定する場合  単位[s]   オプション
                  省略すると、0フレームから再生

  • メソッド  .play(delay = 700,size = 0.33) 括弧内はデフォルト値
       delay  : 再生速度の調整 単位[ms]  オプション
       size    :  動画表示サイズの調整 縦、横の倍率  オプション

使用方法

  • 実行中、端末にメッセージが出力されます。不必要であれば、クラス  内のprint文を全てコメントアウトしてください。

  • 動画の左上に簡単なメッセージが出力されます。googleのフォントをダウンロードし、使用させていただいています。
        NotoSansJP-VariableFont_wght.ttf (日本語)

  • 使用するフォントを変更するには、クラス内の次の箇所を変更してください。(def __add_text() 内)
      # デフォルトフォントファイル
      font_path = 'ファイルパスとファイル名'

  • 動画再生には、次のモジュールを使用しています。
      import cv2

  • 画像に文字列を描画するメソッドには、次のモジュールを使用しています。
      from PIL import Image, ImageFont, ImageDraw

  • 制御(キー入力で次の様に動作)
      一時停止 : スペース
      進む   : f  10フレーム、または10から90フレーム進む
      戻す   : b    10フレーム、または10から90フレーム戻る
       (1秒以内に続けて、1から9までの数字を押すことで、
        10*数値 分進む、戻るとなります)
      次のチャプターへ  : c
      前のチャプターへ  : z
      終了  : 一時停止中に q

  • 使用例

# 動画ファイル
file_path = 'IMG_4938.MOV' # カレントディレクトリ以外の場合は、パスを付加すること

# 開始、チャプター、終了時刻[s]  <-- この時刻で一時停止 省略時は 0フレームから再生
c_time=[62,70,41,48,19,23,66,68]

# スロー再生
m_slow = Slow(file_path,c_time)
m_slow.play(delay = 300) # default :  delay = 700,size = 0.33

スロー再生用のクラス

# スロー再生用クラスの定義
import cv2
import sys
from PIL import Image, ImageFont, ImageDraw
import numpy as np

class Slow:

    def __init__(self,file_path,c_time=[]):
        # 動画ファイルを開く
        self.cap = cv2.VideoCapture(file_path) # <class 'cv2.VideoCapture'>
        # オープンできないときは終了
        if not self.cap.isOpened():
            print('ファイルをオープンできません。')
            sys.exit()

        self.fps = self.cap.get(cv2.CAP_PROP_FPS)                  # fpsを取得
        self.frame_count = self.cap.get(cv2.CAP_PROP_FRAME_COUNT)  # フレーム数を取得 float

        # チャプターが設定されていないとき
        if len(c_time)==0:
            c_time=[0,int(self.frame_count/self.fps)]

        # 時刻をフレーム数に変更
        self.frame_no=[int(n*self.fps) for n in c_time]
        self.frame_no.sort()

    # 動画再生用メソッド
    def play(self,delay = 700,size = 0.33):
        # windowの名前 フレームを表示する時に必要 タイトル欄に表示される
        window_name = 'frame'  

        start_frame=self.frame_no[0]
        end_frame=self.frame_no[-1]
        next_pos=1
        next_frame=self.frame_no[next_pos]
        
        print(f'{self.frame_no}     end = {self.frame_count-1:.0f}')

        # スローで再生
        self.cap.set(cv2.CAP_PROP_POS_FRAMES,start_frame) # 再生開始のフレームをセット
        while True:
            recent_frame = self.cap.get(cv2.CAP_PROP_POS_FRAMES) # 現在のフレーム
            if recent_frame < self.frame_count: # 最終フレームを越えていない
                print('\r'+'\033[2K'*20,end='') #80文字クリア
                print(f'\r再生中 : {recent_frame:.0f} / {next_frame:.0f} [frame]   {recent_frame/self.fps:.3f} [s]   スペースで一時停止',end='') # 有効桁:g
                
            else:
                print('動画の最終フレームを越えました。終了します。')
                cv2.destroyWindow(window_name) # windowを閉じる
                break

            # 1フレームを読み込む
            ret, frame0 = self.cap.read()
            # 読み込み成功
            if ret:
                # フレームの調整
                frame = cv2.resize(frame0, dsize=None, fx=size, fy=size)  # 等比拡大縮小
                #frame=frame0[50:150, 30:170]                          # フレームの部分抽出
                # 文字列の表示
                message = 'スペース: 一時停止\n停止中--> q:終了  b:戻す f:進む  チャプター( c:次 z:前 )  他:再生継続'
                message = message+'\n再生フレーム: '+str(int(recent_frame))+'     next stop: '+str(int(next_frame))
                frame=self.__add_text(frame,message,font_size = 12,font_color=(0, 0, 255, 0),disp_pos=(0,5))
                
                # フレームの表示
                cv2.imshow(window_name, frame)
                cv2.moveWindow(window_name, 0, 0) # 表示位置の調整  (再生中位置を変更できない:要改善)
                
                # スロー再生速度の調整(delay[ms])と一時停止入力待ちを兼ねている
                pause = cv2.waitKey(delay) & 0xFF == ord(' ')

                # 最終フレーム、チャプターをかけるフレーム、スペースキー入力で一時停止させるための処理
                #   停止中 q:終了 b:戻す f:進む c:次 z:前のチャプター 左記以外のキーで再生継続
                if recent_frame >= end_frame or recent_frame >= next_frame or pause: 

                    print(f'\r一時停止 : {recent_frame:.0f} [frame]   {recent_frame/self.fps:.3f} [s](終了: q   進む: f   戻る: b (10フレーム)  次:c  前:z  継続:以外のキー)',end='') # 有効桁:g
                
                    ck = cv2.waitKey(0) # キー入力待ちで、一時停止させる

                    # q で終了
                    if  ck & 0xFF == ord('q'): 
                        print(f'\n終了 : {recent_frame:.0f} [frame] \t{recent_frame/self.fps:.3f} [s]') # 有効桁:g
                        cv2.destroyWindow(window_name) # windowを閉じる
                        break

                    # b で n*10フレーム戻る
                    elif ck & 0xFF == ord('b'):
                        next_frame=recent_frame
                        n=cv2.waitKey(1000)
                        if n>48 and n<60: # 1から9の範囲
                            step=int(chr(n))*10
                            recent_frame-=step
                        else:
                            recent_frame-=10
                        self.cap.set(cv2.CAP_PROP_POS_FRAMES,recent_frame)
                 
                    # f で n*10フレーム進む
                    elif ck & 0xFF == ord('f'):
                        n=cv2.waitKey(1000)
                        if n>48 and n<60: # 1から9の範囲
                            step=int(chr(n))*10
                            recent_frame+=step
                        else:
                            recent_frame+=10
                        self.cap.set(cv2.CAP_PROP_POS_FRAMES,recent_frame)

                    # 次のチャプターへ
                    elif ck & 0xFF == ord('c'):
                        recent_frame=self.frame_no[next_pos]
                        self.cap.set(cv2.CAP_PROP_POS_FRAMES,recent_frame)
                        next_pos+=1
                        if len(self.frame_no)>next_pos:
                            next_frame=self.frame_no[next_pos]
                        else:
                            next_frame=self.frame_no[-1]

                    # 前のチャプターへ
                    elif ck & 0xFF == ord('z'):
                        next_pos-=1 # 直前のチャプターを飛び越させる
                        if next_pos>1:
                            recent_frame=self.frame_no[next_pos-1]
                            self.cap.set(cv2.CAP_PROP_POS_FRAMES,recent_frame)
                            next_frame=self.frame_no[next_pos]
                        else:
                            recent_frame=start_frame
                            self.cap.set(cv2.CAP_PROP_POS_FRAMES,recent_frame)
                            next_frame=self.frame_no[1]                 
              
                # next_frameの修正
                if recent_frame>next_frame:
                    for next_pos in range(len(self.frame_no)-1,0,-1):
                        if recent_frame<self.frame_no[next_pos]:                    
                            next_frame=self.frame_no[next_pos]                

                # 終了フレーム --> 繰り返し再生するための初期設定(開始点に戻す)
                if recent_frame >= end_frame: 
                    self.cap.set(cv2.CAP_PROP_POS_FRAMES,start_frame) # 最初に戻す
                    next_frame = self.frame_no[1]

    # 画像に文字列を描画するメソッド(クラス内で使用)
    def __add_text(self,img, message,font_path='',font_size=24,font_color=(255, 255, 255, 0),disp_pos=(0,0)):
        # デフォルトフォントファイル
        font_path = '/home/spot/Downloads/Noto_Sans_JP/NotoSansJP-VariableFont_wght.ttf'
        # PIL用フォントを定義
        font = ImageFont.truetype(font_path, font_size)
        # PIL用に画像を型変換
        img = Image.fromarray(img) # <class 'numpy.ndarray'>(cv2)  -->  <class 'PIL.Image.Image'>(PIL)
        # 描画用クラスの生成
        draw = ImageDraw.Draw(img) # <class 'PIL.ImageDraw.ImageDraw'>
        # 文字列を描画
        draw.text(disp_pos, message, font=font, fill=font_color) # 位置(横、縦), 挿入文字列, フォント, 文字色(BGR+α)
        return np.array(img) # <class 'numpy.ndarray'>(cv2用)に変換して返す
                
        # 再生中の再生速度の調整(未実装)
        # 再生中の画像サイズの調整(未実装)
        # 再生中の表示位置の調整(未実装)

いいなと思ったら応援しよう!