PythonでMovieから画像を保存するプログラムを作ってみた

Movie Cut Tomo

今日はMovieから画像を保存するプログラムを紹介します。

同じ機能を持った有料無料のソフトは世の中にたくさんあります。

あえて紹介するのは、自分で作ることに意義があるからです。

自分で考え、動くものとして形にすることに価値があります。

これからプログラムを始める人や勉強中の方の参考になればと思います。

ちなみに、30分程度のアニメやドラマのmp4動画でも余裕で処理できます。

結構便利です。

本プログラムの特徴

  • 動画(mp4)から素早く好きな場面を見つけ静止画保存が可能
  • 全フレームを素早く確認する画面と好きな場面をコマ送りで確認する画面の二画面構成
  • Captureできる画像サイズは3種類(デフォルト:1920×1080、その他960×540、640×360)
  • Frame 1のスライダーで全フレームを素早く確認できる
  • Frame 2のコマ送りボタン(<<、>>)で素早く欲しい場面を探せる

実行画面

以下の映像をご覧ください。

プログラム

以下のコードをみてください。

import cv2
import time
import glob
import datetime
import numpy as np
import PySimpleGUI as sg

### window の色を設定 ###
sg.theme('Dark Blue 3')

### 初期パラメータ設定 ###
fn = 0   # Frame 1用の変数
fs = 0   # Frame 2用の変数  

# ファイルを開くポップアップ表示
fname = sg.popup_get_file('File to open')

# ファイル読み込み
f = glob.glob(fname)

### 動画ファイルの読み込み(同じ動画をそれぞれ読み込む)###

# 全体確認用
cap_a = cv2.VideoCapture(f[0])
count_a = cap_a.get(cv2.CAP_PROP_FRAME_COUNT)  # 総フレーム数の取得

# 部分確認用
cap_b = cv2.VideoCapture(f[0])
count_b = cap_b.get(cv2.CAP_PROP_FRAME_COUNT)  # 総フレーム数の取得

### ボタン関係の設定 ###

# テキスト設定
s_moji_1 = sg.Text('Frame 1', size=(10, 1), font=('メイリオ', 12))
s_moji_2 = sg.Text('Frame 2', size=(10, 1), font=('メイリオ', 12))
s_moji_3 = sg.Text('Frame Step', size=(10, 1), font=('メイリオ', 12))
s_moji_4 = sg.Text('capture size', size=(10, 1), font=('メイリオ', 10))

# 線設定
s_sen_1 = sg.Text('_'  * 35)

# スライダー設定
s_Slider_1 = sg.Slider((0, count_a-1), 0, 1, orientation='h', size=(93, 15), key='-frame number 1-')

# 実行ボタン設定
s_button_1 = sg.Submit(button_text='OK', size=(14, 2))
s_button_2 = sg.Submit(button_text='Capture', size=(31, 15))
s_button_3 = sg.Submit(button_text='Quit', size=(20, 20), button_color=('black', '#4adcd6'))
s_button_4 = sg.Submit(button_text='>>', size=(12, 2))
s_button_5 = sg.Submit(button_text='<<', size=(12, 2))

# 画像サイズを指定するインプットボックス
s_input_text_1 = sg.InputText(key='frame_select', size=(15, 3))
s_input_text_2 = sg.InputText(default_text='1', key='fs_step', size=(15, 3))

### 隠れボタン設定 ###
# capture画面サイズの選択
choices_1 = ('1920 x 1080', '960 x 540', '640 x 360')

s_choice_1 = sg.InputCombo(choices_1,
                            default_value='1920 x 1080',
                            key= '-cap size_1-',
                            size=(33, len(choices_1)),
                            enable_events=True)

# 文字出力ウィンドウ設定
s_out_1 = sg.Output(size=(170,20), key='-OUTPUT-')

# image
s_image_1 = sg.Image(filename='', key='-IMAGE_1-')
s_image_2 = sg.Image(filename='', key='-IMAGE_2-')

### 画面レイアウト設定 ###
""" 設定パラメータはフレームでまとめた方が便利 """

# Frame 1 関係
layout_1_0 = sg.Frame('',
                    [
                     [s_moji_1, s_Slider_1]
                    ],
                    relief=sg.RELIEF_SUNKEN
                   )

# Frame 2 関係
layout_1_1 = sg.Frame('',
                    [
                     [s_moji_2, s_input_text_1, s_button_1, s_moji_3, s_input_text_2, s_button_5, s_button_4]
                    ],
                    relief=sg.RELIEF_SUNKEN
                   )

# capture sizeとcaptureボタン
layout_2 = sg.Frame('',
                    [
                     [s_moji_4],
                     [s_choice_1],
                     [s_sen_1],
                     [s_button_2]
                    ],
                    relief=sg.RELIEF_SUNKEN
                   )

# Quitボタン
layout_3 = sg.Frame('',
                    [
                     [s_button_3]
                    ],
                    relief=sg.RELIEF_SUNKEN
                   )

# Frame 1の画像表示
layout_4 = sg.Frame('Frame 1',
                    [
                     [s_image_1]
                    ],
                    relief=sg.RELIEF_SUNKEN
                   )

# Frame 2の画像表示
layout_5 = sg.Frame('Frame 2',
                    [
                     [s_image_2]
                    ],
                    relief=sg.RELIEF_SUNKEN
                   )

### レイアウト設定 ###
layout_0 = [
            [layout_4, layout_5],
            [layout_1_0, layout_1_1],
            [s_out_1, layout_2, layout_3]
           ]

### ウィンドウ生成 ###
window = sg.Window('Movie Cut Tomo', layout_0,
                                    location=(0, 0),
                                    alpha_channel=1.0,
                                    no_titlebar=False,
                                    grab_anywhere=False).Finalize()
window.Maximize()

# 文字出力ウィンドウにFrame 1とFrame 2の画像の総フレーム数を表示
print('frame 1 :', int(count_a-1))
print('frame 2 :', int(count_b-1))

### イベントループ ###
while True:
    
    event, values = window.read(timeout=20)

    if event == 'Quit' or event == sg.WIN_CLOSED:
        break

    is_ok_a, frame_a = cap_a.read()
    is_ok_b, frame_b = cap_b.read()

    cap_a.set(cv2.CAP_PROP_POS_FRAMES, fn)
    cap_b.set(cv2.CAP_PROP_POS_FRAMES, fs)
    
    frame_1 = cv2.resize(frame_a, (960, 540), cv2.INTER_LINEAR)
    frame_2 = cv2.resize(frame_b, (960, 540), cv2.INTER_LINEAR)
    
    if event is None:
        print('exit')
        break
    
    # Frame 1のスライダーを動かしたときに数値が代入される
    if values['-frame number 1-']:
        fn = int(values['-frame number 1-'])

    # Frame 2のOKボタンを押したときに実行される
    if event =='OK':
        if int(values['frame_select']) > count_b-1:
            fs == count_b-1
            print('入力した値は無効です!')
        else:
            fs = int(values['frame_select'])
            print('fs = ', fs)

    # >> ボタンを押したときに実行される
    if event == '>>':
        if int(values['frame_select']) >= count_b-1:
            fs == count_b-1
            print('これ以上のフレームはありません!')

        else:
            if int(values['fs_step']) == 1:
                fs += 1
                print('fs = ', fs)
            else:
                fs += int(values['fs_step'])
                print('fs = ', fs)

    # << ボタンを押したときに実行される
    elif event =='<<':
        if int(values['frame_select']) > count_b-1:
            fs == count_b-1
            print('総フレーム数を超えています!')

        elif int(values['frame_select']) < count_b-1:
            if int(values['fs_step']) == 1:
                fs -= 1
                print('fs = ', fs)
            else:
                fs -= int(values['fs_step'])
                print('fs = ', fs)

        elif int(values['frame_select']) == 0:
            fs == 0
            print('これ以下のフレームはありません!')
        
    ### Capture Buttonを押したときの画像保存 ###
    """ 
    Auto Captureした画像と区別するためファイル名を日付とする
    """
    # 日付の取得
    d_today = datetime.date.today()
    dt_now = datetime.datetime.now()

    # 画像保存のコードを関数化
    def cap_original(f_2):
        cv2.imwrite('./cnn_act/capture/original_image/' +
                        str(d_today) + str("_") +
                        str(dt_now.hour) + str("_") +
                        str(dt_now.minute) + str("_") +
                        str(dt_now.second) + '.jpg', f_2)

    # Captureボタンを押した時に実行される
    if event == 'Capture':
        if values['-cap size_1-']:
            if choices_1[0] == values['-cap size_1-'][:]:
                cap_original(frame_b)

            elif choices_1[1] == values['-cap size_1-'][:]:
                cap_original(frame_2)

            elif choices_1[2] == values['-cap size_1-'][:]:
                frame_2_org = cv2.resize(frame_2, (640, 360), cv2.INTER_LANCZOS4)
                cap_original(frame_2_org)
    
    # 映像をpng画像に変換してwindow画面を更新
    imgbytes = cv2.imencode('.png', frame_1)[1].tobytes()
    window['-IMAGE_1-'].update(data=imgbytes)

    imgbytes_2 = cv2.imencode('.png', frame_2)[1].tobytes()
    window['-IMAGE_2-'].update(data=imgbytes_2)

    if event == 'Quit':
        print('Quit')
        break

cap.release()
cv2.destroyAllWindows()

# ウィンドウの破棄と終了
window.close()

ポイント解説

1.動画ファイルの読み込みを二つに分ける

動画読み込みは、全フレーム確認用の cap_a とコマ送り確認用の cap_b に分けます。

その方がシンプルだからです。

動画読み込みを1回で済ますこともできそうですが、コードが複雑になるのは避けたいです。

2.総フレーム数の取得には以下のコードを使う

このコードで動画の全フレーム数が分かります。

count_a = cap_a.get(cv2.CAP_PROP_FRAME_COUNT) 
count_b = cap_b.get(cv2.CAP_PROP_FRAME_COUNT)

なお、count_a、count_b は 整数とは限りません。

後で計算に利用する際は int(count_a) や int(count_b) としておいた方が良いです。

フレーム数は整数として後の計算に利用するためです。

3.ボタン類やレイアウトは先にまとめて定義しておく

これはPySimpleGUIではおなじみのやり方です。

その方がコードが見やすいからです。

コードはシンプルが一番です。

4.eventとvalues を使い分ける

以下のコードをみてください。

event, values = window.read(timeout=20)

コードの左側の event は一回だけのイベント時に使う変数、values は繰り返しのイベント時に使う変数です。

例えば、以下のコードをみてください。

if values['-frame number 1-']:
   fn = int(values['-frame number 1-'])

このコードは、Frame 1 のスライダーが動いたときにその値が整数として fn に代入されるという意味です。

この場合、values[ ] を使っているため while True: 以降のイベントループ中はスライダーの値が代入され続けることになります。

つまり、スライダーを動かしたときの値は fn に代入され続けることになります。

一方、以下のコードをみてください。

if event =='OK':
    if int(values['frame_select']) > count_b-1:
        fs == count_b-1
        print('入力した値は無効です!')
    else:
        fs = int(values['frame_select'])
        print('fs = ', fs)

このコードは、OKボタンが一回押されてたときだけ以降のコードが実行されるという意味です。

なので、OKボタンを押さないと fs には何の値も代入されません。

まとめ

  • 動画ファイルの読み込みを二つに分ける
  • 総フレーム数の取得には cap_a.set( cv2.CAP_PROP_POS_FRAMES, fn) を使う
  • ボタン類やレイアウトは先にまとめて定義しておく
  • eventとvalues を使い分ける