home about terms twitter

FFmpegとPythonでMP4動画をテキスト付きGIFアニメにする

date: 2020-03-01 | mod: 2020-03-01

FFmpegとPillowを用いて、MP4動画から上のようなGIFアニメーションを作成します。今回のコードでは、GIFアニメには動画からの画像のほかに任意のテキストを追加できるようにします。上の例ではフレームの数だけ□が■になるバーを追加しています。

  • FFmpeg …動画・音声編集のためのフリーソフトウェア(公式サイト
  • Pillow …Pythonの画像処理ライブラリ(公式サイト

FFmpegをあらかじめインストールしている必要があります。インストールがお済みでない場合には上記公式サイトにしたがってインストールしてください。

また、フォントを指定しますので、FreeMon.ttfを実行ファイルと同じディレクトリに入れておきます(FreeMonoのダウンロード -> pillow - github)。

index


コード全体

動作環境:

  • python 3.6.8
  • ffmpeg 2.8.4
  • pillow 6.2.1
  • windows10 64bit
input output
input.mp4 output.bar
import io, subprocess
from PIL import Image, ImageDraw, ImageFont, ImageSequence

subprocess.call("ffmpeg -ss 0 -t 2 -i input.mp4 -r 10 input.gif")
subprocess.call("ffmpeg -i input.gif -vf scale=300:-1 output.gif")

frames = []
base_image = Image.open('output.gif')
wim, him = base_image.size
base_frames = [f.copy() for f in ImageSequence.Iterator(base_image)]

for frame in range(len(base_frames)):

    text = 'frame: ' + ''.join(['■' for num in range(frame)]) + ''.join(['□' for num in range(len(base_frames)-frame-1)])

    font = ImageFont.truetype('FreeMono.ttf', 10)
    wf, hf = font.getsize(text)

    frame = base_frames[frame]
    draw = ImageDraw.Draw(frame)
    textbox = Image.new("RGB", (wim, hf), (255, 255, 255))
    textdraw = ImageDraw.Draw(textbox)
    textdraw.text((0, 0), text, font=font, fill=(0,0,0,0))

    del draw, textdraw

    b = io.BytesIO()
    frame.save(b, format="GIF")
    frame = Image.open(b)

    frame.paste(textbox, (0, 0))
    frames.append(frame)

frames[0].save('output_bar.gif', save_all=True, append_images=frames[1:], loop=0)

重要な箇所を以下に示します。


動画データの変換

subprocess.call("ffmpeg -ss 0 -t 2 -i input.mp4 -r 10 input.gif")
subprocess.call("ffmpeg -i input.gif -vf scale=300:-1 output.gif")

subprocess.call()を介して、FFmpegのコマンドをAnaconda promptから実行しています。
ffmpegはpipやcondaによって配布もされていますが、こちらの実行環境のcondaを通してインストールしてもModuleNotFoundError: No module named 'ffmpeg'となりimport出来ませんでした。そのため、Anaconda prompt上から直接FFmpegを用いています。

実行時のパラメータは以下の通りです。

一つ目の実行パラメータ

パラメータ 概要
-ss 0 開始時間(秒)
-t 2 切り取り時間(秒)
-i input.mp4 入力ファイル名
-r 10 フレームレート(枚数/秒)
input.gif 出力ファイル名

二つ目の実行パラメータ

パラメータ 概要
-i input.gif 入力ファイル名
-vf scale=300:-1 横のサイズを300 pxになるようにオートスケール
-i output.gif 出力ファイル名

inputとしてはMOVファイルなどもご利用いただけます。

画質をより上げたい場合には、フレームレートやscaleの数字を大きくすることで達成できると考えられます。ただし、同時にファイルサイズも増大します。

ここまでの操作により、MP4から任意の時間で切り取ったGIFアニメを作成することができています。以降はさらに、そのGIFアニメにテキストを追加する作業を行います。

画像への情報の追加

frames = []
base_image = Image.open('output.gif')
wim, him = base_image.size
base_frames = [f.copy() for f in ImageSequence.Iterator(base_image)]

編集元となる画像をbase_imageとして読み込んでいます。その後、画像の横(x)と高さ(y)のサイズをwim, him = base_image.sizeで得ています。
その後のbase_framesにあるリスト内包表記では、GIFアニメを構成している画像を一枚ずつ取り出してリストを作成しています。

for frame in range(len(base_frames)):

    text = 'frame: ' + ''.join(['■' for num in range(frame)]) + ''.join(['□' for num in range(len(base_frames)-frame-1)])

    font = ImageFont.truetype('FreeMono.ttf', 10)
    wf, hf = font.getsize(text)

    frame = base_frames[frame]
    draw = ImageDraw.Draw(frame)
    textbox = Image.new("RGB", (wim, hf), (255, 255, 255))
    textdraw = ImageDraw.Draw(textbox)
    textdraw.text((0, 0), text, font=font, fill=(0,0,0,0))

    del draw, textdraw

    b = io.BytesIO()
    frame.save(b, format="GIF")
    frame = Image.open(b)

    frame.paste(textbox, (0, 0))
    frames.append(frame)

frames[0].save('output_bar.gif', save_all=True, append_images=frames[1:], loop=0)

参考:github - pillow: How to add text to GIF? #3128

上記のリンク先を参考にしています。そのため、現時点(2020/03/01)では全ての動作を完全に理解しているわけではありません。

text = 'frame: ' + ''.join(['■' for num in range(frame)]) + ''.join(['□' for num in range(len(base_frames)-frame-1)])では、出力させる任意の情報として、GIFアニメのフレームが現在どこまで進んでいるかを示す変数を構成しています。
理想的には以下のように進むものと考えました。

全8フレーム中

進んだフレーム 表記
0 □□□□□□□□
2 ■■□□□□□□
4 ■■■■□□□□
6 ■■■■■■□□

そのために、forループ中のフレーム番号をframeとして取得し、その数だけ■を追加し、最大で全フレーム数分存在する□を現在のフレーム数分減らす動作を行っています。

    font = ImageFont.truetype('FreeMono.ttf', 10)
    wf, hf = font.getsize(text)

    frame = base_frames[frame]
    draw = ImageDraw.Draw(frame)
    textbox = Image.new("RGB", (wim, hf), (255, 255, 255))
    textdraw = ImageDraw.Draw(textbox)
    textdraw.text((0, 0), text, font=font, fill=(0,0,0,0))

FreeMono.ttfを読み込んだ後に、フォントサイズ(10)を指定しています。このサイズのうち、高さ(hf)をのちに用います。

変数であるframeは、base_frames[frame]とすることでbase_framesリスト内にあるframe番目の画像として置き換えています。
その後、textboxとして、新たな画像を生成させています。色指定(RGB, (255, 255, 255)(=白))のほか、縦と横のサイズをwim(= GIF画像の横サイズ)、hf(= フォントの縦サイズ)で指定しています。これにより、横に細長い白色のイメージが生成されたものとします。

その後text()によって先ほどのフレーム進行状況の情報を入れたtextを記入させています。

    frame.paste(textbox, (0, 0))
    frames.append(frame)

frames[0].save('output_bar.gif', save_all=True, append_images=frames[1:], loop=0)

paste()で、先ほどのテキストを含んだイメージをGIF画像に貼り付けています。貼り付け位置は左上(0, 0)としています。最後のsave()で各種パラメータを設定したうえでGIFアニメとして出力させます。

出力

これにより得られる出力は以下の通りです。本記事のはじめにも示しましたが、再度記載します。

python-pil-ffmpeg-movie-gif

目的の画像が出力されたことを確認できました。
サイズが抑えられているため画質も抑えられています。より高画質なデータが必要な場合には、FFmpeg使用段階で前述の通りフレームレートやscaleの数字を大きくしてください。