FFmpegとPythonでMP4動画をテキスト付きGIFアニメにする
FFmpegとPillowを用いて、MP4動画から上のようなGIFアニメーションを作成します。今回のコードでは、GIFアニメには動画からの画像のほかに任意のテキストを追加できるようにします。上の例ではフレームの数だけ□が■になるバーを追加しています。
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アニメとして出力させます。
出力
これにより得られる出力は以下の通りです。本記事のはじめにも示しましたが、再度記載します。
目的の画像が出力されたことを確認できました。
サイズが抑えられているため画質も抑えられています。より高画質なデータが必要な場合には、FFmpeg使用段階で前述の通りフレームレートやscaleの数字を大きくしてください。