Start_python’s diary

ふたり暮らし

アラフィフ夫婦のフリーランスプラン

PythonでGIF編集ソフトが完成しました(動画ファイルを読み込んでGIFへの保存も可能)

はじめに

GIFファイルを開いて編集(再生、カット、トリミング)ができるソフトです。

f:id:Start_python:20200113152439g:plain

保存したGIFファイルがこちらです。

f:id:Start_python:20200114130504g:plain

 

使い方

実行ファイルがあるフォルダにdataフォルダを作ってdataフォルダの中にGIFファイルを用意します。

ソフトを起動するとGIFファイルの一覧が出ます。ファイル名をクリックすると画面が変わってGIFが編集できます。

上のボタン

「<」・・・タイトル一覧に戻ります

保存」・・GIFファイルを保存します(保存ファイル名はtest.gifにしています)

下のボタン

「|」・・・最初(1コマ目)に戻ります

|」・・・1コマ戻ります

「▶」・・・再生します(「」・・・停止します)

「|」・・・1コマ進みます

トリミング」・・トリミングの範囲を指定します

「前カット」・・現在のコマより前を削除します

「後カット」・・現在のコマより後ろを削除します

「元に戻す」・・カットした部分を元に戻します(トリミングは戻りません)

 

まとめ

よく似たフリーソフトはいくつもあると思いますが、細かい部分を手直しできるのでプログラミングの練習もかねて今必要な部分だけ作成してみました。
今回でGIF編集については終わりにしたいと思います。また欲しい機能がでてくれば続きを作るかもしれません。

files = glob.glob('./data/*.gif') を files = glob.glob('./data/*.mp4) に変更して
動画ファイルを読み込めば、動画から一部を切り取ってGIFに変換もできます。

 

少し長いですがプログラムコードを公開します。

プログラムのコード

# フル画面を解除して画面の幅と高さを設定
from kivy.config import Config
Config.set('graphics', 'fullscreen', 0)
Config.set('graphics', 'width', 320)
Config.set('graphics', 'height', 568)
Config.set('graphics', 'resizable', 1)

import os
import glob
import cv2
import numpy as np
from PIL import Image
from moviepy.editor import ImageSequenceClip

from kivy.app import App
from kivy.uix.boxlayout import BoxLayout
from kivy.uix.gridlayout import GridLayout
from kivy.uix.button import Label
from kivy.uix.button import Button
from kivy.uix.image import Image as Img
from kivy.graphics.texture import Texture
from kivy.properties import StringProperty, ObjectProperty
from kivy.clock import Clock
from kivy.graphics import Color
from kivy.graphics import Line
from kivy.uix.popup import Popup

from kivy.core.text import LabelBase, DEFAULT_FONT
from kivy.resources import resource_add_path

resource_add_path('c:/Windows/Fonts')
LabelBase.register(DEFAULT_FONT, 'msgothic.ttc')


class CustomLayout(BoxLayout):
    pass


class MainScreen(BoxLayout):
    image_texture = ObjectProperty(None)

    def __init__(self, **kwargs):
        super().__init__(**kwargs)
        self.ids.button1.text = ''
        self.ids.button2.text = '>'
        self.ids.button3.text = '前カット'
        self.ids.button4.text = '後カット'
        self.ids.button5.text = '元に戻す'

        self.max_img = 1
        self.lines = []
        self.flg = 0

        files = glob.glob('./data/*.gif')
        for file in files:

            gif = cv2.VideoCapture(file)
            is_success, img = gif.read()

            image = np.array(img)
            image = image[:, :, [2, 1, 0]]    # BGR <-> RGB
            image = Image.fromarray(image)

            texture = Texture.create(size=image.size) 
            texture.blit_buffer(image.tobytes())
            texture.flip_vertical()

            self.ids.sv.add_widget(Img(texture = texture, size_hint_x=None, width=80))
            btn = Button(text=os.path.basename(file), on_release=self.on_command,
                    size_hint_y=None, height=80, color=(0,0,0,1), background_color=(1,1,1,0.1))
            self.ids.sv.add_widget(btn)

    def on_slider(self):
        try:
            image = Image.fromarray(self.images[int(self.ids.slider1.value)-1])

            texture = Texture.create(size=image.size) 
            texture.blit_buffer(image.tobytes())
            texture.flip_vertical()

            self.image_texture = texture
        except:
            pass

    def on_command(self,btn):
        self.ids.label1.text = btn.text
        self.ids.button1.text = '<'
        self.ids.button2.text = '保存'
        self.ids.button8.text = '▶'
        self.ids.button10.text = 'トリミング'

        # 読み込み
        gif = cv2.VideoCapture('./data/' + btn.text)
        self.fps = gif.get(cv2.CAP_PROP_FPS)  # fpsは1秒あたりのコマ数

        # 編集
        self.images = []
        i = 0
        while True:
            is_success, img = gif.read()
            if not is_success:
                self.max_img = i
                break

            self.images.append(img)
            i += 1

        self.images = np.array(self.images)
        self.images = self.images[:, :, :, [2, 1, 0]]    # BGR <-> RGB

        self.ids.slider1.max = self.max_img

        image = Image.fromarray(self.images[0])

        texture = Texture.create(size=image.size) 
        texture.blit_buffer(image.tobytes())
        texture.flip_vertical()

        self.image_texture = texture

        self.ids.slider1.value = self.ids.slider1.min

        self.ids.carousel.load_next()

    def on_btn1(self):
        self.ids.carousel.load_previous()
        self.ids.button1.text = ''
        self.ids.button2.text = '>'
        if self.ids.button8.text == '■':
            self.ids.button8.text = '▶'
            self.event.cancel()

    def on_btn2(self):
        if self.ids.button2.text == '保存':
            content = YesNoPopUp(yes=self.save, no=self.no, text='保存します。よろしいですか?')
            self.popup = Popup(title="ファイルの保存", content=content, size_hint=(0.9, 0.5), auto_dismiss=False)
            self.popup.open()
        
        self.ids.button1.text = '<'
        self.ids.button2.text = '保存'
        self.ids.carousel.load_next()

    def on_btn3(self):
        self.ids.slider1.min = self.ids.slider1.value

    def on_btn4(self):
        self.ids.slider1.max = self.ids.slider1.value

    def on_btn5(self):
        self.ids.slider1.min = 1
        self.ids.slider1.max = self.max_img

    def on_btn6(self):
        self.ids.slider1.value = 1
        self.on_slider()

    def on_btn7(self):
        self.ids.slider1.value -= 1
        self.on_slider()

    def on_btn8(self):
        if self.ids.button8.text == '▶':
            self.ids.button8.text = '■'
            self.event = Clock.schedule_interval(self.update, 1 / self.fps)
        else:
            self.ids.button8.text = '▶'
            self.event.cancel()

    def on_btn9(self):
        self.ids.slider1.value += 1
        self.on_slider()

    def on_btn10(self):
        content = YesNoPopUp(yes=self.yes, no=self.no, text='範囲を指定してください')
        self.popup = Popup(title="トリミング", content=content, size_hint=(0.9, 0.5), auto_dismiss=False)
        self.popup.open()

    def update(self, dt):
        if self.ids.slider1.value >= self.ids.slider1.max:
            self.ids.slider1.value = self.ids.slider1.min
        else:
            self.ids.slider1.value += 1
        self.on_slider()

    def on_image1_down(self, touch):
        if self.flg == 0:
            return

        self.x1 = touch.x
        self.y1 = touch.y
        if len(self.lines)>0:
            for line in self.lines:
                self.ids.image1.canvas.remove(line)
            self.lines = []
        with self.ids.image1.canvas:
            touch.ud['line'] = Line(points=[self.x1, self.y1], close='True')
            self.lines.append(touch.ud['line'])

    def on_image1_move(self, touch):
        if self.flg == 0:
            return

        self.x2 = touch.x
        self.y2 = touch.y
        for line in self.lines:
            self.ids.image1.canvas.remove(line)
        self.lines = []
        with self.ids.image1.canvas:
            Color(0, 0, 0)
            touch.ud['line'] = Line(points=[self.x1, self.y1, self.x2, self.y1,
                                            self.x2, self.y2, self.x1, self.y2],
                                            close='True')
            self.lines.append(touch.ud['line'])
            Color(1, 1, 1)
            touch.ud['line'] = Line(points=[self.x1, self.y1, self.x2, self.y1,
                                            self.x2, self.y2, self.x1, self.y2],
                                            dash_offset=5, dash_length=3,
                                            close='True')
            self.lines.append(touch.ud['line'])

    def on_image1_up(self):
        if self.flg == 0:
            return

        content = YesNoPopUp(yes=self.trimming, no=self.no, text='トリミングします。よろしいですか?')
        self.popup = Popup(title="トリミング", content=content, size_hint=(0.9, 0.5), auto_dismiss=False)
        self.popup.open()

    def yes(self):
        self.popup.dismiss()
        if len(self.lines)>0:
            for line in self.lines:
                self.ids.image1.canvas.remove(line)
            self.lines = []
        self.flg = 1

    def no(self):
        self.popup.dismiss()
        if len(self.lines)>0:
            for line in self.lines:
                self.ids.image1.canvas.remove(line)
            self.lines = []
        self.flg = 0

    def trimming(self):
        self.popup.dismiss()
        if len(self.lines)>0:
            for line in self.lines:
                self.ids.image1.canvas.remove(line)
            self.lines = []
        self.flg = 0

        self.img_x1 = int((self.x1 - (self.ids.image1.pos[0] \
            + ((self.ids.image1.size[0] - self.ids.image1.norm_image_size[0]) / 2))) \
            * (self.ids.image1.texture_size[0] / self.ids.image1.norm_image_size[0]))
        self.img_y1 = int(((self.ids.image1.pos[1] \
            + ((self.ids.image1.size[1] - self.ids.image1.norm_image_size[1]) / 2)) \
            + self.ids.image1.norm_image_size[1] - self.y1) \
            * (self.ids.image1.texture_size[1] / self.ids.image1.norm_image_size[1]))
        self.img_x2 = int((self.x2 - (self.ids.image1.pos[0] \
            + ((self.ids.image1.size[0] - self.ids.image1.norm_image_size[0]) / 2))) \
            * (self.ids.image1.texture_size[0] / self.ids.image1.norm_image_size[0]))
        self.img_y2 = int(((self.ids.image1.pos[1] \
            + ((self.ids.image1.size[1] - self.ids.image1.norm_image_size[1]) / 2)) \
            + self.ids.image1.norm_image_size[1] - self.y2) \
            * (self.ids.image1.texture_size[1] / self.ids.image1.norm_image_size[1]))

        if self.img_x1 < 0:
            self.img_x1 = 0
        if self.img_y1 < 0:
            self.img_y1 = 0
        if self.img_x2 > self.ids.image1.texture_size[0]:
            self.img_x2 = self.ids.image1.texture_size[0]
        if self.img_y2 > self.ids.image1.texture_size[1]:
            self.img_y2 = self.ids.image1.texture_size[1]

        if self.img_x1 < self.img_x2 and self.img_y1 < self.img_y2:
            self.images = self.images[:, self.img_y1:self.img_y2, self.img_x1:self.img_x2]
        if self.img_x1 > self.img_x2 and self.img_y1 < self.img_y2:
            self.images = self.images[:, self.img_y1:self.img_y2, self.img_x2:self.img_x1]
        if self.img_x1 < self.img_x2 and self.img_y1 > self.img_y2:
            self.images = self.images[:, self.img_y2:self.img_y1, self.img_x1:self.img_x2]
        if self.img_x1 > self.img_x2 and self.img_y1 > self.img_y2:
            self.images = self.images[:, self.img_y2:self.img_y1, self.img_x2:self.img_x1]

        self.on_slider()

    def save(self):
        self.popup.dismiss()

        self.images = np.array(self.images)
        self.images = self.images[int(self.ids.slider1.min-1):int(self.ids.slider1.max-1)]
        self.images = list(self.images)

        clip = ImageSequenceClip(self.images, fps=self.fps)
        clip.write_gif('./data/test.gif')


class YesNoPopUp(BoxLayout):
    text = StringProperty()
    yes = ObjectProperty(None)
    no = ObjectProperty(None)


class TestApp(App):
    def build(self):
        self.title = 'テスト'
        return MainScreen()

if __name__ == '__main__':
    TestApp().run()

kvファイル(test.kv)

<MainScreen>:
    CustomLayout:
        orientation: "vertical"

        BoxLayout:
            orientation: "vertical"

            BoxLayout:
                Button:
                    id: button1
                    size_hint_x: 0.2
                    text: "<"
                    on_release: root.on_btn1()
                Label:
                    id: label1
                    size_hint_x: 0.6
                    text: "FILE NAME"
                    color: 0, 0, 0, 1
                Button:
                    id: button2
                    size_hint_x: 0.2
                    text: ">"
                    on_release: root.on_btn2()

            Carousel:
                id: carousel
                size_hint_y: 10
                scroll_timeout: 0
                anim_move_duration: 0.1
                ScrollView:
                    GridLayout:
                        id: sv
                        cols: 2
                        size_hint_y: None
                        orientation: "vertical"
                        height: self.minimum_height
                BoxLayout:
                    orientation: "vertical"
                    Image:
                        id: image1
                        size_hint_y: 10
                        texture: root.image_texture
                        on_touch_down: root.on_image1_down(args[1])
                        on_touch_move: root.on_image1_move(args[1])
                        on_touch_up: root.on_image1_up()
                    Slider:
                        canvas.before:
                            Color:
                                rgba: 0, 0, 0, 1
                            Rectangle:
                                pos: self.pos
                                size: self.size
                        id: slider1
                        min: 1
                        value_track: True
                        value_track_color: 1,0,0,1
                        on_touch_up: root.on_slider()
                        on_touch_move: root.on_slider()
                    BoxLayout:
                        Button:
                            background_color: 0,0,0,1
                            text: "|◀"
                            on_release: root.on_btn6()
                        Button:
                            background_color: 0,0,0,1
                            text: "◀|"
                            on_release: root.on_btn7()
                        Button:
                            id: button8
                            background_color: 0,0,0,1
                            font_size: 24
                            text: u"▶"
                            on_release: root.on_btn8()
                        Button:
                            background_color: 0,0,0,1
                            text: "|▶"
                            on_release: root.on_btn9()
                        Button:
                            id: button10
                            background_color: 0,0,0,1
                            text: ""
                            on_release: root.on_btn10()

            BoxLayout:
                Button:
                    id: button3
                    text: "3"
                    on_release: root.on_btn3()
                Button:
                    id: button4
                    text: "4"
                    on_release: root.on_btn4()
                Button:
                    id: button5
                    text: "5"
                    on_release: root.on_btn5()

<CustomLayout>:
    canvas.before:
        Color:
            rgba: 1, 1, 1, 1
        Rectangle:
            pos: self.pos
            size: self.size

<YesNoPopUp>
    BoxLayout:
        #size_hint_y: None
        orientation: 'vertical'

        Label:
            size_hint: 1, 0.8
            text: root.text

        BoxLayout:
            size_hint: 1, 0.2
            orientation: 'horizontal'
            Button:
                size_hint: 0.5, 1
                text: 'OK'
                on_release: root.yes()
            Button:
                size_hint: 0.5, 1
                text: 'Cancel'
                on_release: root.no()

 

 

↓よかったらポチッとしていってください。

にほんブログ村 IT技術ブログ Pythonへ
にほんブログ村