Qiita Teams that are logged in
You are not logged in to any team

Log in to Qiita Team
Community
OrganizationEventAdvent CalendarQiitadon (β)
Service
Qiita JobsQiita ZineQiita Blog
10
Help us understand the problem. What is going on with this article?

More than 1 year has passed since last update.

@kurodae

kivyを使ってGUIプログラミング ~その3 動画とシークバー~

はじめに

前回の記事では、プログレスバーについて色々書きました。
今回の記事では、自作で動画プレイヤー的な物を作って見ようと思います。(音は出ません。。。)元々は、動画解析(物体検出とか)の結果を可視化するために、kivyで用意されている動画プレイヤーではなく、カスタマイズ性のあるプレイヤーを作ろうとしたのがきっかけです。
タイトルにあるように、動画プレイヤーのシークバーの実装に手こずったので、情報共有としてこの記事を書いています。

環境構築

はじめにでも触れたように、動画解析結果の可視化を目的としていますので、動画(画像)解析用のライブラリopencvをインストールします。

私の動作環境は、以下の通りです。

OSX 10.14.6
Python 3.7.2

pipを使っているようであれば、下記のコマンドだけでインストールできます。

pip install python-opencv

シークバーとは

wikiから引用します。

シークバーとは、音楽・動画再生ソフトなどに備わる機能のひとつで、データの再生箇所を表示する機能のことである。音楽やムービーの始めから終わりまでのうちどこまでを再生しているのかが「スライダー」の位置によって視覚的に把握できる、スライダーをマウスで直接移動させ、それによって任意の箇所から再生を始めることができる、といった利点がある。

シークバーはスライダーの一種ということです。ではスライダーとは?
wikiから引用します。

スライダー — スクロールバーに似ているが、何らかの値を設定するのに使われるウィジェットであって、スクロールのためのものではない。

kivyは、スライダーのウィジェットが用意されているため、こちらをシークバーに仕立て上げます。

kivy.uix.sliderの使い方

こんな感じになります。
シークバー.gif

ソースは以下の通りです。


from kivy.app import App
from kivy.lang import Builder
from kivy.uix.boxlayout import BoxLayout
from kivy.clock import Clock

Builder.load_string('''
<MySlider>
    orientation: 'vertical'
    Label : 
        text: "{}".format(slider.value)
        font_size: slider.value

    Slider: 
        id: slider
        step: 1
        min: 200
        max: 500

    Button: 
        text: "press"
        on_press: root.move_slider_start()

''')

class MySlider(BoxLayout):
    def __init__(self, **kwargs):
        super(MySlider, self).__init__(**kwargs)

    def move_slider_start(self):
        Clock.schedule_interval(self.move_slider, 1 / 60)

    def move_slider(self, dt):
        slider = self.ids['slider']
        if slider.max > slider.value:
            slider.value += 1
        else:
            return False

class sliderTest(App):

    def build(self):
        return MySlider()

sliderTest().run()

スライダーの値を、画面上部のラベルのテキストとフォントに紐づけて、スライダーを動かすとラベルも変化するといったソースです。また、下のボタンを押すとクロックでスライダーの値を連続的に更新するような処理も加えました(あとで動画プレイヤーで似たようなことをします。)

kv言語では、Sliderのidを使ってLabelのテキストを更新しています。

    Label : 
        text: "{}".format(slider.value)
        font_size: slider.value

    Slider: 
        id: slider # idです。kv言語やPythonで呼び出すことができます。
        step: 1 # スライダーを動かす時の最小値。設定しないとめちゃくちゃfloatな値が出ます
        min: 200 # スライダーの最小値
        max: 500 # スライダーの最大値

また、自動でスライダーを動かす処理は、ウィジェットに割り当てたidが連想配列として格納されている"ids"から下記のような感じでウィジェットを選択してパラメータを変更します。前回の記事の通り、for文では動作させることができないので、Clockでスライダーの値を変更し画面描画も更新しています。


    def move_slider(self, dt):
        slider = self.ids['slider'] #ここ!
        if slider.max > slider.value:
            slider.value += 1
        else:
            return False

作ろうとしている物

今回作ろうとしているものは下図のようなイメージです。

動画プレイヤー.png

最低限の機能のみの動画プレイヤーといった感じです。動画を読み込んで、再生と、任意の再生場所の指定、現在のフレーム数の確認だけです。

動画は、opencvで扱っていく感じです。

ソース

VideoApp.py
from kivy.app import App
from kivy.lang import Builder
from kivy.uix.boxlayout import BoxLayout
from kivy.graphics.texture import Texture
from kivy.properties import ObjectProperty
from kivy.clock import Clock

from kivy.uix.floatlayout import FloatLayout
from kivy.uix.popup import Popup

import cv2

Builder.load_file('VideoApp.kv')

#動画選択ポップアップ
class LoadDialog(FloatLayout):
    load = ObjectProperty(None)
    cancel = ObjectProperty(None)

class MyVideoPlayer(BoxLayout):
    image_texture = ObjectProperty(None)
    image_capture = ObjectProperty(None)

    def __init__(self, **kwargs):
        super(MyVideoPlayer, self).__init__(**kwargs)
        self.flagPlay = False # 動画が再生されているか
        self.now_frame = 0 # シークバー用の動画の再生フレームを確認する変数
        self.image_index = [] # シークバー用のopencv画像を保存する配列

    # 動画をロードするポップアップ
    def fileSelect(self):
        content = LoadDialog(load = self.load, cancel = self.dismiss_popup)
        self._popup = Popup( title="File Select", content=content, size_hint=(0.9,0.9))
        self._popup.open()

    # 動画ファイルのロード
    def load (self, path, filename):
        txtFName = self.ids['txtFName']
        txtFName.text = filename[0]
        self.image_capture = cv2.VideoCapture(txtFName.text)
        self.sliderSetting()
        self.dismiss_popup()

    # ポップアップを閉じる
    def dismiss_popup(self):
        self._popup.dismiss()

    # シークバーの設定
    def sliderSetting(self):
        count = self.image_capture.get(cv2.CAP_PROP_FRAME_COUNT)
        self.ids["timeSlider"].max = count

        # 一度動画を読み込み、全てのフレームを配列に保存する
        while True:
            ret, frame = self.image_capture.read()
            if ret:
                self.image_index.append(frame)

            else:
                self.image_capture.set(cv2.CAP_PROP_POS_FRAMES, 0)
                break

    # 動画再生
    def play(self):
        self.flagPlay = not self.flagPlay
        if self.flagPlay == True:
            self.image_capture.set(cv2.CAP_PROP_POS_FRAMES, self.now_frame)
            Clock.schedule_interval(self.update, 1.0 / self.image_capture.get(cv2.CAP_PROP_FPS))
        else:
            Clock.unschedule(self.update)

    # 動画再生のクロック処理
    def update(self, dt):
        ret, frame = self.image_capture.read()
        # 次のフレームが読み込めたら
        if ret:
            self.update_image(frame)
            time = self.image_capture.get(cv2.CAP_PROP_POS_FRAMES)
            self.ids["timeSlider"].value = time
            self.now_frame = int(time)

    # シークバー
    def siderTouchMove(self):
        Clock.schedule_interval(self.sliderUpdate, 0)

    # シークバーを動かした時の画面描画処理
    def sliderUpdate(self, dt):
        # シークバーの値と、再生フレームの値が異なった時
        if self.now_frame != int(self.ids["timeSlider"].value):
            frame = self.image_index[self.now_frame-1]
            self.update_image(frame)
            self.now_frame = int(self.ids["timeSlider"].value)

    def update_image(self, frame):
        ##############################
        #ここに画像処理系のソースを書く!!
        ##############################

        # 上下反転
        buf = cv2.flip(frame, 0)
        image_texture = Texture.create(size=(frame.shape[1], frame.shape[0]), colorfmt='bgr')
        image_texture.blit_buffer(buf.tostring(), colorfmt='bgr', bufferfmt='ubyte')
        video = self.ids['video']
        video.texture = image_texture

class TestVideo(App):

    def build(self):
        return MyVideoPlayer()

TestVideo().run()

kvファイル

VideoApp.kv
<MyVideoPlayer>:
    orientation: 'vertical'
    padding: 0
    spacing: 1

    BoxLayout:
        orientation: 'horizontal'
        padding: 0
        spacing: 1
        size_hint: (1.0, 0.1)

        TextInput:
            id: txtFName
            text: ''
            multiline: False

        Button:
            text: 'file load'
            on_press: root.fileSelect()

    BoxLayout:
        orientation: 'horizontal'
        padding: 0
        spacing: 1

        Image:
            id: video

    BoxLayout:
        orientation: 'horizontal'
        padding: 0
        spacing: 1
        size_hint: (1.0, 0.1)
        Slider:
            id: timeSlider
            value: 0.0
            max: 0.0
            min: 0.0
            step: 1
            on_touch_move: root.siderTouchMove()

    BoxLayout:
        orientation: 'horizontal'
        padding: 0
        spacing: 1
        size_hint: (1.0, 0.1)

        ToggleButton:
            size_hint: (0.2, 1)
            text: 'Play'
            on_press: root.play()

        Label:
            size_hint: (0.2, 1)
            text: str(timeSlider.value) + "/" + str(timeSlider.max)

<LoadDialog>:
    BoxLayout:
        size: root.size
        pos: root.pos
        orientation: 'vertical'
        FileChooserListView:
            id: filechooser
            path: "./"

        BoxLayout:
            size_hint_y : None
            height : 30
            Button:
                text: 'Cancel'
                on_release: root.cancel()

            Button:
                text: 'Load'
                on_release: root.load(filechooser.path, filechooser.selection)

実行してみると、こんな感じで再生できたり、シークバーが動かせたりします。
動画はこちらのサイトからお借りいたしました。
Player.gif
※あまりにも画質がよかったり、長い動画だったりすると、シークバーの処理で固まってしまうと思うので、短めの動画でお試しください。

少し解説

動画の再生自体は、opencvのVideoCuptureと同じ用量で行います。使ったことがある方はなくわかると思いますが、while文で1フレームごと動画のコマを読み込んで表示するみたいな感じのやつで(こんな感じです)。繰り返しになりますが、forやwhileを使うと固まってしまうので、Clockを用います。

    # 動画再生のクロック処理
    def update(self, dt):
        ret, frame = self.image_capture.read()
        # 次のフレームが読み込めたら
        if ret:
            self.update_image(frame) # 画面に写す処理
            time = self.image_capture.get(cv2.CAP_PROP_POS_FRAMES) #  シークバー用にフレーム数を取得
            self.ids["timeSlider"].value = time # シークバーの値に再生フレーム数を代入
            self.now_frame = int(time) # シークバーを動かす時用

また、kivyに画像を表示する時には、Textureをいうクラスを用いてblit_bufferでバッファー化して画像を扱います。この時、opencvの画像データは上下逆さまになってしまうため、反転する処理を追加します。
こうすることで、普通にopenCVの動画再生と同じような感覚で動画再生機能を実装できます。今回は、画像処理は行いませんが、ここにopencvなどの処理を加えれば簡単に処理結果の可視化等もできます。

    def update_image(self, frame):
        ##############################
        #ここに画像処理系のソースを書く!!
        ##############################

        # 上下反転
        buf = cv2.flip(frame, 0)
        image_texture = Texture.create(size=(frame.shape[1], frame.shape[0]), colorfmt='bgr')
        image_texture.blit_buffer(buf.tostring(), colorfmt='bgr', bufferfmt='ubyte')
        video = self.ids['video']
        video.texture = image_texture

動画のシークバーでは、当初はopencvのVideoCupterクラスのset関数(再生フレームを指定する関数)から、Sliderの値を動画のフレーム番号に当てはめて、シークバーを実装しようとしていました。しかし、実装してみるとめちゃくちゃ動作が重くなってしまいました。原因、set関数が非常に重い処理のようで他の実装方法を模索しました。

結果として、配列にそのままopencvの画像データを格納して、Sliderの値と画像の配列の添字を紐づけることで簡単に実装することができました。そのため、動画の読み込みを行う時に、1度動画を再生して配列に代入する処理を設けています。

プログラム内では、一度読み込んだVideoCuptureを使いまわしているため、動画の再生位置を最初の状態に戻すself.image_capture.set(cv2.CAP_PROP_POS_FRAMES, 0)を指定してあげないと、動画の再生ができなくなります。

    # シークバーの設定
    def sliderSetting(self):
        count = self.image_capture.get(cv2.CAP_PROP_FRAME_COUNT) # 動画のフレーム数を取得
        self.ids["timeSlider"].max = count # 動画のフレーム数をスライダーの最大値に代入

        # 一度動画を読み込み、全てのフレームを配列に保存する
        while True:
            ret, frame = self.image_capture.read()
            if ret:
                self.image_index.append(frame)

            else:
                #最後のフレームまで読み込んだら、最初のフレームに戻す
                self.image_capture.set(cv2.CAP_PROP_POS_FRAMES, 0)
                break

参考文献

大変お世話になりました。

Python KivyでOpencvやPillowで画像を表示する方法
Textureの扱い等

Pythonをはじめよう 第7回 ファイルを選択して動画再生 - あきらちんの技術メモ
動画でかなりお世話になりました。

10
Help us understand the problem. What is going on with this article?
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
10
Help us understand the problem. What is going on with this article?