動画から静止画を抽出する

記録を残すのに、よくカメラを使います。

iPhotoやPicassaを使えば、イベントごとに写真をまとめておくのも簡単だし、ざっと一覧できるし、スクリーンセーバーでランダムに出てくるように設定しておくと、忘れた頃に出てきて感情が揺さぶられます。とてもすてき。公開したければそこからFlickrなどへのパスも発達してるので簡単です。

これで最近問題なのが、動画の割合が増えていることです。動画でないと取りきれない情報もたくさんあるので、動画で撮ること自体は良いことだと思うのですが、動画を動画のままにした場合、容量をたくさん食います。オレのiPhotoマシンは古い白のMacBookで、256GBのSSDに換装してはあるものの、使ってるカメラのE-P2が吐くAVIファイルは1GBあたり4分くらい。お話になりません。ポイントポイントだけ動画で撮って…とかの工夫をしてると何でも取り逃がすに決まってるので、ダラダラ撮り流したいです。

最初に使った方法は、ffmpegで最初の1フレームを静止画として抽出してやる方法です。静止画になっていれば容量は食いませんし、何があったかを思い出すだけなら静止画でも十分です。動画部分はファイルサーバに置いておいて、必要なときに取り出せば良い。一覧性も損なわれない。これで7割は満足しました。ffmpegとjheadを使ったワンライナー:

pushd /nobu ; for i in 201?_??_??/P???????.AVI ; do JPG=${i/\./}.JPG ; if [ ! -f $JPG ] ; then ffmpeg -i $i -f image2 -an -y -vframes 1 -s 1280x720  $JPG ; touch --reference=$i $JPG ; jhead -mkexif -dsft $JPG ; fi ; done ; popd

読みにくいから複数行に展開すると、

pushd /nobu
for i in 201?_??_??/P???????.AVI
  do JPG=${i/\./}.JPG
  if [ ! -f $JPG ]
    then ffmpeg -i $i -f image2 -an -y -vframes 1 -s 1280x720  $JPG
    touch --reference=$i $JPG
    jhead -mkexif -dsft $JPG
  fi
done
popd

です。

…中学生にも判るように説明したいと思います。

"/nobu" はTimemachineのバックアップから外してあるディレクトリで、写真や動画はここに取り込むようになってます。まずはpushdでここに移動。pushdはcdと同じでカレントディレクトリを移動しますが、それまで居た場所を覚えていてpopdで元の場所に戻れます。

/nobu以下には、前に使ってたIXYに付いてきたCanonのユーティリティが"2013_01_06"みたいな形式のディレクトリを掘って写真を格納してくれてます。カメラのメディアを突っ込んだ時に自動起動するプログラムが日付分けしてくれるというのが便利。

"for i in 201?_??_??/P???????.AVI"は、201?_??_??/P???????.AVIというパターンにマッチしたファイルのそれぞれに処理をかけるという書き方。"for 変数 in 指定物; do 処理 ; done"と書くことで、「指定物」それぞれに「処理」をかけるループ処理のプログラムが書けます。

"JPG=${i/\./}.JPG"は、bashのパターン置換("${変数/パターンA/パターンB}"でパターンAをパターンBに置き換える機能)を使い、AVIファイルのファイル名からJPEGファイルのファイル名を作ってます。たとえば元がP01122034.AVIというファイルであれば、ドットを削除して最後に.JPGを付けてP01122034AVI.JPGというファイル名を作ります。AVIというのを残すのは、iPhotoなどで見た時に動画ファイルの存在を示すためです。当初はJPG=${i/.AVI/AVI.JPG}と書いてました。

次の"ffmpeg -i $i -f image2 -an -y -vframes 1 -s 1280x720 $JPG" の部分がキモの静止画抽出です。オリンパスの動画ファイルを拾うパターンP???????.AVIにマッチしたファイル名はfor iで取りましたが、これを表示するには$iと書いてやる必要があります。というわけで変数$iには動画のファイル名が入ってるので、ffmpegの入力ファイル(-iで指定)にこれをあてがってやります。"-s"で指定する出力JPEGの画像サイズはいつも撮ってるHD動画の1280x720に固定。最後の$JPGは出力ファイル名です。あとは静止画を1枚だけ抽出するのに必要な指定で、"-f image2"でJPEG出力、"-an"は音声オフ(静止画なので)、"-y"は同じファイル名の古いファイルがあっても上書き、"-vframes 1"は1フレームだけ読んで変換する、という指定。

JPEGファイルはこれで出力できますが、これだとタイムスタンプが変換時刻現在になってしまうので、touchコマンドで書き換えます。このとき--reference=$iで元ファイルの日時を指定。最後にjheadコマンドでexifにタイムスタンプを書き込んでやります。これでiPhotoのイベントがバラバラにならずにすみます。

これでだいたい満足してましたが、使ってる内に不満が。それは抽出される静止画が最初の1フレーム目であること。動画を撮る時は、シャッターチャンスのど真ん中で撮ったりしません。カメラを回しておいて何かが起きるのを待ちます。だから最初の1フレーム目は、ゴミ、とまでは言わないものの、当を得た画像であることは稀です。

それではどこらへんを抽出すると良いでしょうか。撮り始めると勢いがついて止めるのを躊躇する傾向があるので、後半もあまり良くない場合が多いです。半分より前が良い。1/3とか最初の10%が過ぎたあたりとか、そこらへんが良さそう。

ffmpegには開始フレーム指定のオプションもあるので、こうした時間から抽出すること自体は可能です。"-ss 時間"で指定してやればよい。時間の部分は時分秒で00:00:00.00の形式。ではファイルの1割とか3割とかの時間を見て、このオプションを入れればいいわけです。

これは手動で指定するのは簡単だけどアホらしいタスクです。たくさんあったら非常に大変。そして案外と自動化しにくいタスクでもあります。ffmpegには動画変換に便利なオプションは揃ってますが、静止画関係はあんまり無いので、このような頭の良い処理はない。ちらっと検索した感じ、こうした時間計算をしてくれるコマンドラインツールも見当たらない。

そんなわけで、ffmpegが吐いてくれる情報を取って時間や解像度を表示するツールをPythonで書きました。Pythonにはdatetimeというモジュールがあり、時間の計算がやりやすくなっているので、ffmpegから時間情報を取る→計算→00:00:00.00形式で出力、というのがすぐ出来ます。timecalcという名前にして、

timecalc 演算子 数字 動画ファイル名

の形で使えます。"timecalc x 0.1 動画ファイル名"で、その動画の1割の時間が出力されます。ソースはこちら

これを上のスクリプトに組み込んで、動画がオリンパスのAVIでもIS12Tのmp4でも大丈夫なようにすると、いま使ってる形になります:

pushd /nobu ; for i in 201?_??_??/{WP*Z.mp4,P???????.AVI} ; do JPG=${i/\./}.JPG ; FROM=`timecalc x 0.1 $i` ; if [ ! -f $JPG ] ; then ffmpeg -i $i -f image2 -an -y -ss $FROM -vframes 1 -s 1280x720  $JPG ; touch --reference=$i $JPG ; jhead -mkexif -dsft $JPG ; fi ; done ; popd

バッチリ!
展開すると:

pushd /nobu
for i in 201?_??_??/{WP*Z.mp4,P???????.AVI}
  do JPG=${i/\./}.JPG
  FROM=`timecalc x 0.1 $i`
  if [ ! -f $JPG ]
    then ffmpeg -i $i -f image2 -an -y -ss $FROM -vframes 1 -s 1280x720  $JPG
    touch --reference=$i $JPG
    jhead -mkexif -dsft $JPG
  fi
done
popd

というかんじです。
ちなみに、ちゃんと独立したスクリプトにするならディレクトリは外から指定すべきだし、ファイル形式のチェックは他のやり方がよさそう。あと動画の解像度が違ってても良いようにすべきでしょうね。
あとffmpegを時間指定すると遅い!なんか全部読んでるみたい。ここらをどうにかできたら完璧ですけど、まあオレはcronで走らせるからいいかな。