PythonでGIF編集ソフトが完成しました(動画ファイルを読み込んでGIFへの保存も可能)
はじめに
GIFファイルを開いて編集(再生、カット、トリミング)ができるソフトです。
保存したGIFファイルがこちらです。
使い方
実行ファイルがあるフォルダに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()
↓よかったらポチッとしていってください。