よくあるしつもんとかいとう?
かな~り久々の投稿も例によって ffmpeg ネタ。
Contents
ffmpeg で静止画から動画
モチベーションと良くある回答
わたしは「魔改造皆無の Windows 7」ユーザのままなので、「Movie Maker じゃやりたいことでけんのよねぇ」となると毎度ハタと困ってしまう。
ワタシはこういうことがしたかったのだ:
- 音声ファイル(例えば英語教材)を持っている。
- その音声ファイルには背景動画がないかもしくは気に入らない。
- ので収集済みの手持ちの関連静止画たちを背景動画に仕立て上げたい。
Windows 7 に付属の Movie Maker でも「あたかもこのニーズに耐える」かのように見えて、これが全然ダメ。まず Movie Maker には明確な主従があって、「静止画たちで作る動画に BGM をつける」ようにしか作られてない。つまり「音声に背景動画をつける」ように使うのは不可能もしくは難儀だ。これは実際に使ってみればわかる。のみならず、Movie Maker ではどうやら各入力静止画を動画で何秒間使うかの制御が出来ない…、みたいだ。少なくとも Windows 7 に付属のヤツではこれを見つけられてない。
となった場合に何を考えるかと言えば、ひとつには「もっと高機能なや~つ」を探し回る(そして疲れ果てる)か、例によって「めんどいけど ffmpeg かぁ」と諦めるかの二択だ。前者も興味はあるが今回は後者で。やりたいことの性質上ちょっとは高等な GUI が向いているのが明らか、だけれど、探し回って疲れるのも今回は体力もないし。
なお、PyAV という手もあるが今回は剥き身の ffmpeg で。
てなわけでさて、と、ffmpeg image to video duration みたいな検索をかけると「いつもの」質問と回答がわんさかヒットする:
1 [me@host: ~]$ ffmpeg -r 1/5 -start_number 2 -i img%03d.png -c:v libx264 -r 30 -pix_fmt yuv420p out.mp4
1 [me@host: ~]$ ffmpeg -loop 1 -i image.png -c:v libx264 -t 15 -pix_fmt yuv420p -vf scale=320:240 out.mp4
これでやりたいことが出来ないってことではないのだけれど、少なくとも「そのものズバリ」でないことはわかるであろう。こういう「大量の静止画を一気にバカちょんで動画に」したいんではなくて、「(まがりなりにも)ちゃんとした編集」をしたいのだ。つまりは最低でも「入力静止画の各々の duration は自由に任意に」制御したいわけである。
なお、いつものように実験結果をお見せするが、入力に使ったイメージは pexels の CC0 ライセンスのものたち。
出来てみたファーストシーズン
要するに filter_complex を地道にごにょればいいんでしょ、って発想でスタートしてみるわけだ。なので「静止画を入力としてこれを任意フレーム群として複製する術があればあるだろうか」と考えるわけなのだが、やぱ~り loop しかなさげ。そうなのか、そうなのだろうなぁ。
たとえばこんな:
1 #! /bin/sh
2
3 #
4 # simple
5 #
6 ffmpeg -y \
7 -i cat1.jpg \
8 -i cat2.jpg \
9 -i cat3.jpg \
10 -c:v libx264 \
11 -pix_fmt yuv420p \
12 -r 25 \
13 -filter_complex "
14 [0]scale=960:720,setsar=1,loop=loop=25:size=2[v1];
15 [1]scale=960:720,setsar=1,loop=loop=50:size=2[v2];
16 [2]scale=960:720,setsar=1,loop=loop=75:size=2[v3];
17
18 [v1][v2][v3]concat=n=3[v]" \
19 -map '[v]' cat1.mp4
20
21
22 #
23 # with fading in/out
24 #
25 ffmpeg -y \
26 -i cat1.jpg \
27 -i cat2.jpg \
28 -i cat3.jpg \
29 -c:v libx264 \
30 -pix_fmt yuv420p \
31 -r 25 \
32 -filter_complex "
33 [0]scale=960:720,setsar=1,loop=loop=25:size=2,fade=in:0:5,fade=out:20:5[v1];
34 [1]scale=960:720,setsar=1,loop=loop=50:size=2,fade=in:0:5,fade=out:45:5[v2];
35 [2]scale=960:720,setsar=1,loop=loop=75:size=2,fade=in:0:5,fade=out:75:5[v3];
36
37 [v1][v2][v3]concat=n=3[v]" \
38 -map '[v]' cat2.mp4
さらっと書いてるが、「苦労時間」の 99.8% がこの「loop=loop=75:size=2
」であった。正確に覚えてないが体感では数時間これだけでうんうん唸ってた。「説明不足」だ、いつものように:
Set maximal size in number of frames. Default is 0.
ナニイッテンダ?
出来てみた実例から「無理やり解釈」したこれの意味はたぶんこう:
- loop は静止画用ではなくて。
- なので「このフレームからこのフレームまでを繰り返すなるなり」の意味なろう
- つまりは繰り返しの末端フレームを指すだのか?
- なればなんで size=1 でないのや。
なんだかよくわからんけれど、「一枚絵=フレーム一枚」「このフレーム一枚とは開始フレーム0に相当しろう」てことなら「末端」を指示するのに 0 か 1 なら解せるがなぜに 2 か。ともあれ「解せる最大 + 1」を渡せば期待通りになる、らしい。なんなんだこれは。
size がなんだか妙で 1 多く渡さなければならないことを受け容れるならば loop の残りを「解釈」することは出来ろう。「1×75」であれば、今フレームレートを 25 としているので、これは3秒。動画入力で使いたい場合は「3フレームを10回繰り返せ」たければどうすればいいかはすぐわかろう。
fade も微妙に鬱陶しいが説明も鬱陶しいので 公式ドキュメント参照。なんにせよ今の場合 loop での指定に引きずられるので、違うことがしたくなった場合に編集箇所が多くてめげてしまう。
ともあれ、上の例の結果は、これだぁ:
出来てみたセカンドシーズン(アスペクト比、音声)
話を先に進める前に、ごまかした部分2点。
一つ目が「アスペクト比がバラバラの入力だよね、ふつー」。二つ目が、モチベーションに書いたこと、「音声ファイルを手持ちで背景動画をつけたいんや」な件。
前者:
1 #! /bin/sh
2
3 #
4 # simple
5 #
6 ffmpeg -y \
7 -i cat4.jpg \
8 -i cat2.jpg \
9 -i cat3.jpg \
10 -c:v libx264 \
11 -pix_fmt yuv420p \
12 -r 25 \
13 -filter_complex "
14 [0]scale=w=960:h=720:force_original_aspect_ratio=1,
15 pad=960:720:(ow-iw)/2:(oh-ih)/2,
16 setsar=1,
17 loop=loop=25:size=2[v1];
18 [1]scale=w=960:h=720:force_original_aspect_ratio=1,
19 pad=960:720:(ow-iw)/2:(oh-ih)/2,
20 setsar=1,
21 loop=loop=50:size=2[v2];
22 [2]scale=w=960:h=720:force_original_aspect_ratio=1,
23 pad=960:720:(ow-iw)/2:(oh-ih)/2,
24 setsar=1,
25 loop=loop=75:size=2[v3];
26
27 [v1][v2][v3]concat=n=3[v]" \
28 -map '[v]' cat1.mp4
29
30
31 #
32 # with fading in/out
33 #
34 ffmpeg -y \
35 -i cat4.jpg \
36 -i cat2.jpg \
37 -i cat3.jpg \
38 -c:v libx264 \
39 -pix_fmt yuv420p \
40 -r 25 \
41 -filter_complex "
42 [0]scale=w=960:h=720:force_original_aspect_ratio=1,
43 pad=960:720:(ow-iw)/2:(oh-ih)/2,
44 setsar=1,
45 loop=loop=25:size=2,fade=in:0:5,fade=out:20:5[v1];
46 [1]scale=w=960:h=720:force_original_aspect_ratio=1,
47 pad=960:720:(ow-iw)/2:(oh-ih)/2,
48 setsar=1,
49 loop=loop=50:size=2,fade=in:0:5,fade=out:45:5[v2];
50 [2]scale=w=960:h=720:force_original_aspect_ratio=1,
51 pad=960:720:(ow-iw)/2:(oh-ih)/2,
52 setsar=1,
53 loop=loop=75:size=2,fade=in:0:5,fade=out:75:5[v3];
54
55 [v1][v2][v3]concat=n=3[v]" \
56 -map '[v]' cat2.mp4
音声については例えば4つ目の入力(インデクスは3)として sample_audio.mp4 を使うとして:
1 #! /bin/sh
2
3 #
4 # simple
5 #
6 ffmpeg -y \
7 -i cat4.jpg \
8 -i cat2.jpg \
9 -i cat3.jpg \
10 -i sample_audio.mp4 \
11 -c:v libx264 \
12 -pix_fmt yuv420p \
13 -r 25 \
14 -filter_complex "
15 [0]scale=w=960:h=720:force_original_aspect_ratio=1,
16 pad=960:720:(ow-iw)/2:(oh-ih)/2,
17 setsar=1,
18 loop=loop=25:size=2[v1];
19 [1]scale=w=960:h=720:force_original_aspect_ratio=1,
20 pad=960:720:(ow-iw)/2:(oh-ih)/2,
21 setsar=1,
22 loop=loop=50:size=2[v2];
23 [2]scale=w=960:h=720:force_original_aspect_ratio=1,
24 pad=960:720:(ow-iw)/2:(oh-ih)/2,
25 setsar=1,
26 loop=loop=75:size=2[v3];
27
28 [v1][v2][v3]concat=n=3[v]" \
29 -map '[v]' -map '3:a' cat1.mp4
30
31
32 #
33 # with fading in/out
34 #
35 ffmpeg -y \
36 -i cat4.jpg \
37 -i cat2.jpg \
38 -i cat3.jpg \
39 -i sample_audio.mp4 \
40 -c:v libx264 \
41 -pix_fmt yuv420p \
42 -r 25 \
43 -filter_complex "
44 [0]scale=w=960:h=720:force_original_aspect_ratio=1,
45 pad=960:720:(ow-iw)/2:(oh-ih)/2,
46 setsar=1,
47 loop=loop=25:size=2,fade=in:0:5,fade=out:20:5[v1];
48 [1]scale=w=960:h=720:force_original_aspect_ratio=1,
49 pad=960:720:(ow-iw)/2:(oh-ih)/2,
50 setsar=1,
51 loop=loop=50:size=2,fade=in:0:5,fade=out:45:5[v2];
52 [2]scale=w=960:h=720:force_original_aspect_ratio=1,
53 pad=960:720:(ow-iw)/2:(oh-ih)/2,
54 setsar=1,
55 loop=loop=75:size=2,fade=in:0:5,fade=out:75:5[v3];
56
57 [v1][v2][v3]concat=n=3[v]" \
58 -map '[v]' -map '3:a' cat2.mp4
このままだと音声ファイルが最も長いとすれば生成結果は最長に合わせられることになる。これが目的の振る舞いかどうかはやりたいことによるだろう。このままでも q をタイプすれば止められるので、少しずつ完成させていくスタイルで作業していくつもりなら、このままでもいいかもしれない。そうではなく映像の方(短い方)に合わせたいなら、コマンドラインオプションの -shortest
を使えばいい:
1 #! /bin/sh
2
3 #
4 # simple
5 #
6 ffmpeg -y \
7 -i cat4.jpg \
8 -i cat2.jpg \
9 -i cat3.jpg \
10 -i sample_audio.mp4 \
11 -c:v libx264 \
12 -pix_fmt yuv420p \
13 -r 25 \
14 -filter_complex "
15 [0]scale=w=960:h=720:force_original_aspect_ratio=1,
16 pad=960:720:(ow-iw)/2:(oh-ih)/2,
17 setsar=1,
18 loop=loop=25:size=2[v1];
19 [1]scale=w=960:h=720:force_original_aspect_ratio=1,
20 pad=960:720:(ow-iw)/2:(oh-ih)/2,
21 setsar=1,
22 loop=loop=50:size=2[v2];
23 [2]scale=w=960:h=720:force_original_aspect_ratio=1,
24 pad=960:720:(ow-iw)/2:(oh-ih)/2,
25 setsar=1,
26 loop=loop=75:size=2[v3];
27
28 [v1][v2][v3]concat=n=3[v]" \
29 -map '[v]' -map '3:a' -shortest cat1.mp4
30
31
32 #
33 # with fading in/out
34 #
35 ffmpeg -y \
36 -i cat4.jpg \
37 -i cat2.jpg \
38 -i cat3.jpg \
39 -i sample_audio.mp4 \
40 -c:v libx264 \
41 -pix_fmt yuv420p \
42 -r 25 \
43 -filter_complex "
44 [0]scale=w=960:h=720:force_original_aspect_ratio=1,
45 pad=960:720:(ow-iw)/2:(oh-ih)/2,
46 setsar=1,
47 loop=loop=25:size=2,fade=in:0:5,fade=out:20:5[v1];
48 [1]scale=w=960:h=720:force_original_aspect_ratio=1,
49 pad=960:720:(ow-iw)/2:(oh-ih)/2,
50 setsar=1,
51 loop=loop=50:size=2,fade=in:0:5,fade=out:45:5[v2];
52 [2]scale=w=960:h=720:force_original_aspect_ratio=1,
53 pad=960:720:(ow-iw)/2:(oh-ih)/2,
54 setsar=1,
55 loop=loop=75:size=2,fade=in:0:5,fade=out:75:5[v3];
56
57 [v1][v2][v3]concat=n=3[v]" \
58 -map '[v]' -map '3:a' -shortest cat2.mp4
ふんがー
めんどくせー。やりたいことが単純だということは、「指示」も単純「であるべき」なわけで、「写真1を1秒、写真2を2.3秒、…」だけをつらつらと書き連ねるだけで目的のものが出来てほしかろう、てなわけで、雑でもいいからと Python スクリプトにしてしまおうか、つー話。
ワタシの性格的にもこれを読んでいるあなたにとっても「将来的には」なことは考えるであろうから、「指示」は多少は拡張性を見越したものにしたいので、こんな json から始めてみることにする:
1 {
2 "inputs": {
3 "audio": {"file": "Babel1.mp4"},
4 "images": [
5 {"file": "stills_2/302.png", "duration": 2.5},
6 {"file": "stills_2/303.png", "duration": 2.5},
7 {"file": "stills_2/304.png", "duration": 2.5}
8 ]
9 }
10 }
やりたそうなこと、まで考えると「任意の時刻に挿入」までやりたくなりそうなのだが、この場合ギャップ萌え埋めが煩雑になるので今回はやらない。今は入力の静止画を連続で連結するだけのものにしておく。けれどこの json 形式ならば、そうしたくなった場合の拡張はしやすい。
このような指示 json を入力にし、作る python スクリプトは上で挙げたような bash スクリプトを生成するものとしとく。subprocess でダイレクトに ffmpeg を起こすことも出来るけど、出来上がった bash スクリプトを手作業で編集してしまえるメリットもなんだかんだ大きいので、「ダイレクトで起動できる方が唯一絶対的な正義」ってわけではない。
作る Python スクリプトの主たる責務は要するに「一つ何かを変えたければ芋づるでほかが引きずられて要編集となる」な人間にとって煩雑な一連の行為(計算)である。ゆえ、そこそこめんどくて、ソースコードは整理する気がなければ読みづらいものになるので覚悟。ひとまず飾り気のなく、柔軟性もない最もシンプルな初版:
1 #! /bin/env python
2 # -*- coding: utf-8 -*-
3 from __future__ import unicode_literals
4 from __future__ import absolute_import
5
6
7 #{
8 # "inputs": {
9 # "audio": {"file": "Babel1.mp4"},
10 # "images": [
11 # {"file": "stills_2/302.png", "duration": 2.5},
12 # {"file": "stills_2/303.png", "duration": 2.5},
13 # {"file": "stills_2/304.png", "duration": 2.5}
14 # ]
15 # }
16 #}
17 import sys
18 import json
19 import math
20
21
22 def _dur_to_nframes(dur, fps):
23 return int(math.ceil(dur * fps))
24
25
26 def _build(defs):
27 aud = defs["inputs"]["audio"]["file"]
28 imgs = defs["inputs"]["images"]
29
30 fc = []
31 for i, img in enumerate(imgs):
32 # each image
33 fc.append("""\
34 [{idx}]
35 scale=w=960:h=720:force_original_aspect_ratio=1,
36 pad=960:720:(ow-iw)/2:(oh-ih)/2,
37 setsar=1,
38 loop=loop={loop}:size=2
39 [v{idx}]
40 """.format(
41 idx=i,
42 loop=_dur_to_nframes(img["duration"], fps=25)))
43
44 # concat
45 fc.append(
46 "{}concat=n={}[v]".format(
47 "".join(["[v%d]" % idx for idx in range(len(imgs))]),
48 len(imgs)
49 ))
50
51 # build bash script
52 script = r"""#! /bin/sh
53
54 ffmpeg -y \
55 {image_inputs} {audio_input} \
56 -c:v libx264 \
57 -pix_fmt yuv420p \
58 -r 25 \
59 -filter_complex "
60 {filter_complex}
61 " -map '[v]' -map '{audidx}:a' -shortest {outname}
62 """.format(
63 image_inputs=" ".join(["-i '%s'" % img["file"] for img in imgs]),
64 audio_input="-i '{}'".format(aud),
65 filter_complex=";\n".join(fc),
66 audidx=len(imgs),
67 outname="result.mp4")
68
69 return script
70
71
72 if __name__ == '__main__':
73 import argparse
74
75 parser = argparse.ArgumentParser()
76 parser.add_argument("definition")
77 args = parser.parse_args(sys.argv[1:])
78
79 defs = json.loads(open(args.definition).read())
80 #print(json.dumps(defs, indent=2))
81 print(_build(defs))
ひとまず「始められる」状態ではあり、今すぐ出来る拡張をして「今日のワタシ」ニーズに耐えるものにはすぐ出来るけど、いったんここまでの時点で本日 21:00 時点での投稿を締めておく。「ちゃんとしたもの」にする気はあんまりないけれど、「少なくともこれは欲しい」水準はあるので、それについては追記の形で掲載予定。しばしお待たれな。
ふんがー・ゼロ (12/16 スクリプトの微拡張)
予告どおり基本的に自分が当座の用を足せればいいというノリはいつも通りであって、真面目な整理するつもりは毛頭ない、なレベルの、だけれども「少しは使える」やーつ:
1 #! c:/Python35/python.exe
2 # -*- coding: utf-8 -*-
3 from __future__ import unicode_literals
4 from __future__ import absolute_import
5
6
7 #{
8 # "inputs": {
9 # "audio": {"file": "Babel1.mp4"},
10 # "images": [
11 # {"file": "stills_2/302.png", "duration": 2.5},
12 # {"file": "stills_2/303.png", "duration": 2.5},
13 # {"file": "stills_2/304.png", "duration": 2.5}
14 # ]
15 # }
16 #}
17 import sys
18 import json
19 import math
20 import os
21
22
23 def _dur_to_nframes(dur, fps):
24 return int(math.ceil(dur * fps))
25
26
27 def _is_still(fn):
28 # TODO: more flexible
29 return os.path.splitext(fn)[1] in (".png", ".jpg")
30
31
32 def _build(defs, outprm):
33 aud = defs["inputs"]["audio"]["file"]
34 imgs = defs["inputs"]["images"]
35
36 fc = []
37 for i, img in enumerate(imgs):
38 # each image
39 content = ""
40 if _is_still(img["file"]):
41 dur = _dur_to_nframes(img.get("duration", 1.0), fps=outprm["fps"])
42 content = "loop=loop={}:size=2,".format(dur)
43 else: # movie
44 # TODO: when the duration is not specified, take it from the media.
45 # (maybe we must use "ffprobe".)
46 if "duration" in img:
47 dur = img["duration"]
48 content = "trim=0.000:{},setpts=PTS-STARTPTS,".format(dur)
49 #
50 extra = ""
51 if img.get("v_extra_filter"):
52 extra = img["v_extra_filter"] + ","
53 #
54 fade = ""
55 fadea = []
56 if "fade_in" in img:
57 fid = _dur_to_nframes(img["fade_in"], fps=outprm["fps"])
58 fadea.append("fade=in:0:{}".format(fid))
59 if "fade_out" in img:
60 fod = _dur_to_nframes(img["fade_out"], fps=outprm["fps"])
61 fadea.append("fade=out:{}:{}".format(dur - fod, fod))
62 if fadea:
63 fade = ",".join(fadea) + ","
64 fc.append("""\
65 [{idx}]
66 fps={fps},
67 scale=w={w}:h={h}:force_original_aspect_ratio=1,
68 pad={w}:{h}:(ow-iw)/2:(oh-ih)/2,
69 {content}
70 {extra}
71 {fade}
72 setsar=1
73 [v{idx}]
74 """.format(
75 idx=i,
76 fps=outprm["fps"],
77 w=outprm["width"],
78 h=outprm["height"],
79 extra=extra,
80 fade=fade,
81 content=content))
82
83 # concat
84 fc.append(
85 "{}concat=n={}[v]".format(
86 "".join(["[v%d]" % idx for idx in range(len(imgs))]),
87 len(imgs)
88 ))
89
90 # build bash script
91 script = r"""#! /bin/sh
92
93 ffmpeg -y \
94 {image_inputs} {audio_input} \
95 -c:v libx264 \
96 -pix_fmt yuv420p \
97 -filter_complex "
98 {filter_complex}
99 " -map '[v]' -map '{audidx}:a' -shortest {outfilename}
100 """.format(
101 image_inputs=" ".join(["-i '%s'" % img["file"] for img in imgs]),
102 audio_input="-i '{}'".format(aud),
103 filter_complex=";\n".join(fc),
104 audidx=len(imgs),
105 outfilename=outprm["outfilename"])
106
107 return script
108
109
110 if __name__ == '__main__':
111 import argparse
112
113 parser = argparse.ArgumentParser()
114 parser.add_argument("definition")
115 parser.add_argument("--width", type=int, default=1920)
116 parser.add_argument("--height", type=int, default=1080)
117 parser.add_argument("--framerate", type=float, default=25)
118 parser.add_argument("--outfilename", type=str, default="result.mp4")
119 args = parser.parse_args(sys.argv[1:])
120
121 defs = json.loads(open(args.definition).read())
122 #print(json.dumps(defs, indent=2))
123 print(_build(defs, {
124 "fps": args.framerate,
125 "width": args.width,
126 "height": args.height,
127 "outfilename": args.outfilename
128 }))
入力の json は少しだけ記述できることが増えてこんな具合:
1 {
2 "inputs": {
3 "audio": {"file": "audio.mp4"},
4 "images": [
5 {"file": "../images/aaa01-01.png", "duration": 0.5, "fade_in": 0.2, "fade_out": 0.2},
6 {"file": "../images/aaa01-02.png", "duration": 0.5, "fade_in": 0.2, "fade_out": 0.2},
7 {"file": "../images/aaa01-03.png", "duration": 0.5, "fade_in": 0.2, "fade_out": 0.2},
8 {"file": "../images/blank.png", "duration": 3},
9 {"file": "../images/aaa02.png", "duration": 1, "fade_out": 0.5},
10 {"file": "../images/blank.png", "duration": 1},
11
12 {"file": "../images/aaa02.png", "duration": 93, "fade_out": 0.5},
13 {"file": "../images/bbb04.png", "duration": 0.5, "fade_in": 0.2, "fade_out": 0.2},
14 {"file": "../images/aaa02.png", "duration": 0.5, "fade_out": 0.2},
15 {"file": "../images/bbb01.png", "duration": 0.5, "fade_in": 0.2, "fade_out": 0.2},
16
17 {"file": "../images/aaa02.png", "duration": 21, "fade_in": 0.5},
18 {"file": "../images/aaa02.png", "duration": 10, "v_extra_filter": "edgedetect=mode=colormix,avgblur=5"},
19
20 {"file": "../images/aaa02.png", "duration": 125.5, "fade_in": 0.5, "fade_out": 0.5},
21 {"file": "movie.mp4"},
22 {"file": "../images/aaa02.png", "duration": 106, "fade_in": 0.5, "fade_out": 9}
23 ]
24 }
25 }
新たに出来るようにしたのは以下:
- フェードイン/フェードアウトを指定出来るように。
- 動画もぶちこめるように。
- 自前でフィルターを追加出来るように(v_extra_filter)。
フェードイン/アウトは静止画についてしか使えない(unbound local で落ちる、今のところ)。使えるように拡張するのは「難しくはないが面倒」。入力の duration をプログラムが知る必要があるから、てだけで、これは簡単で面倒。
改造前から出来てなくて、改造する気は今のところないのが、最初の投稿時にも書いた「開始時刻指定」。今のままだと「積み上げ」で作っていくしかないので、動画の後ろの方が前の方の変更の影響を常に受け続けるので、しじょーにメンテしづらい。が、これに対応するプログラムは多分あなたが考えるよりも数倍煩雑。ほかで紹介した alignment_info_by_sound_track はまさにこれをしてるが、かなり入り組んでいる。
なお、「動画もぶちこめるように」がアタシには嬉しい誤算だった。この記事の見出しの通り、もともと「静止画に基づいて動画を作る」から始めたわけだが、ふと「あら、出来るわ」と気付き、して、実際に静止画も動画も区別なく使えると、かなり楽しげな動画を比較的簡単に作れる。
ちなみにフェードイン/アウトと「v_extra_filter」ってのが「ソフトウェアのあるべき姿」からは少し遠いことはわかってやっておる。「v_extra_filter」だけで済ますとか、あるいは「v_extra_filter」の裏ということなら「v_predefined_filter」みたいな対応が綺麗である。けど今はどうでもいい。「ワタシの今」に耐えれば良いのであるし、「やりたいことから考えりゃ、一番ニーズが多いのは確実にフェードイン/アウトでしょ、だからこれが浮いてても気にしない気にしない」てことだわ。
12/20 些細な修正
こういうリビジョン管理対象にしないようなゴミスクリプトの「改造」をちまちまブログ内で更新していこうとするのも「キリがない」んで、小さな修正を上げてくのも躊躇してしまうわ。て気分もあるんだけど、あんまりにあんまりなよろしくなげっぷりなままなのも気持ちも良くないので。
上のスクリプト、「空白を含む出力ファイル名がダメ」てのと、あと「音声ファイルが必須なのもなぁ」の2点を修正:
1 #! c:/Python35/python.exe
2 # -*- coding: utf-8 -*-
3 from __future__ import unicode_literals
4 from __future__ import absolute_import
5
6
7 #{
8 # "inputs": {
9 # "audio": {"file": "Babel1.mp4"},
10 # "images": [
11 # {"file": "stills_2/302.png", "duration": 2.5},
12 # {"file": "stills_2/303.png", "duration": 2.5},
13 # {"file": "stills_2/304.png", "duration": 2.5}
14 # ]
15 # }
16 #}
17 import sys
18 import json
19 import math
20 import os
21
22
23 def _dur_to_nframes(dur, fps):
24 return int(math.ceil(dur * fps))
25
26
27 def _is_still(fn):
28 # TODO: more flexible
29 return os.path.splitext(fn)[1] in (".png", ".jpg")
30
31
32 def _build(defs, outprm):
33 aud = ""
34 if "audio" in defs["inputs"]:
35 aud = defs["inputs"]["audio"]["file"]
36 imgs = defs["inputs"]["images"]
37
38 fc = []
39 for i, img in enumerate(imgs):
40 # each image
41 content = ""
42 if _is_still(img["file"]):
43 dur = _dur_to_nframes(img.get("duration", 1.0), fps=outprm["fps"])
44 content = "loop=loop={}:size=2,".format(dur)
45 else: # movie
46 # TODO: when the duration is not specified, take it from the media.
47 # (maybe we must use "ffprobe".)
48 if "duration" in img:
49 dur = img["duration"]
50 content = "trim=0.000:{},setpts=PTS-STARTPTS,".format(dur)
51 #
52 extra = ""
53 if img.get("v_extra_filter"):
54 extra = img["v_extra_filter"] + ","
55 #
56 fade = ""
57 fadea = []
58 if "fade_in" in img:
59 fid = _dur_to_nframes(img["fade_in"], fps=outprm["fps"])
60 fadea.append("fade=in:0:{}".format(fid))
61 if "fade_out" in img:
62 fod = _dur_to_nframes(img["fade_out"], fps=outprm["fps"])
63 fadea.append("fade=out:{}:{}".format(dur - fod, fod))
64 if fadea:
65 fade = ",".join(fadea) + ","
66 fc.append("""\
67 [{idx}]
68 fps={fps},
69 scale=w={w}:h={h}:force_original_aspect_ratio=1,
70 pad={w}:{h}:(ow-iw)/2:(oh-ih)/2,
71 {content}
72 {extra}
73 {fade}
74 setsar=1
75 [v{idx}]
76 """.format(
77 idx=i,
78 fps=outprm["fps"],
79 w=outprm["width"],
80 h=outprm["height"],
81 extra=extra,
82 fade=fade,
83 content=content))
84
85 # concat
86 fc.append(
87 "{}concat=n={}[v]".format(
88 "".join(["[v%d]" % idx for idx in range(len(imgs))]),
89 len(imgs)
90 ))
91
92 # build bash script
93 script = r"""#! /bin/sh
94
95 ffmpeg -y \
96 {image_inputs} {audio_input} \
97 -c:v libx264 \
98 -pix_fmt yuv420p \
99 -filter_complex "
100 {filter_complex}
101 " -map '[v]' {audmap} -shortest '{outfilename}'
102 """.format(
103 image_inputs=" ".join(["-i '%s'" % img["file"] for img in imgs]),
104 audio_input="-i '{}'".format(aud) if aud else "",
105 filter_complex=";\n".join(fc),
106 audmap="-map '{audidx}:a'".format(audidx=len(imgs)) if aud else "",
107 outfilename=outprm["outfilename"])
108
109 return script
110
111
112 if __name__ == '__main__':
113 import argparse
114
115 parser = argparse.ArgumentParser()
116 parser.add_argument("definition")
117 parser.add_argument("--width", type=int, default=1920)
118 parser.add_argument("--height", type=int, default=1080)
119 parser.add_argument("--framerate", type=float, default=25)
120 parser.add_argument("--outfilename", type=str, default="result.mp4")
121 args = parser.parse_args(sys.argv[1:])
122
123 defs = json.loads(open(args.definition).read())
124 #print(json.dumps(defs, indent=2))
125 print(_build(defs, {
126 "fps": args.framerate,
127 "width": args.width,
128 "height": args.height,
129 "outfilename": args.outfilename
130 }))