デスクトップを連続撮影してタイムラプス動画にしてみる

きっかけはこちらのページ。最近スクリーンショットのことばかり追跡していたら、コメントで面白い使い道を教えてもらったのだ。(u3さん、ありがとう!)

実験

  • まず、command-shift-3で適当な間隔をあけて10枚くらいデスクトップのスクリーンショットを撮影しておく。
    • 変化のないデスクトップを動画にしても「動かない動画」=「写真」と同等で面白みがないので、
    • いつもの操作をしながら、そのついでに撮影しておく方が、あとで見て面白いはず。
  • 10枚撮影したら、ファイル名を連番に変更しておく。
    • 10枚くらいなら頑張って手作業でOK。
    • 例:001.png 002.png ... 010.png
    • 撮影時間順に古い方が001、新しい方が010。
  • ところで自分のRetina環境では、デスクトップのスクリーンショットは3840×2400という馬鹿でかいピクセル数になってしまう。
  • さすがに動画にした時の再生負荷を考えると気が引けるので、適当な大きさにリサイズしておいた。
  • Finderで画像ファイルを選択してコピー、ターミナルで「sips -Z 960 」と入力して、それに続けてペースト、そしてreturnキーで実行した。
$ sips -Z 960 001.png 002.png ... 010.png
  • リサイズまで完了したら、デスクトップにworkフォルダを作って、そこに入れておいた。
  • 以上で、動画にするための事前準備はすべて完了。
  • いよいよ、ffmpegを使って静止画を動画にしてみる。
諸々インストール

その前に、ffmpegがインストールされていない場合...

  • ちなみに、ffmpegはHomebrew経由でインストールした。
  • さらには、HomebrewのためにはXcodeが必要。
  • XcodeApp Storeからダウンロードできる。
  • Xcodeをインストールしたら、以下のツールもインストールしておいた。
  • Xcode >> 環境設定...(Preferrence...)>> Downloads >> Command Line Tools

20121127085720

  • これでHomebrewをインストールして、
$ ruby -e "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/master/install)"
  • やっとffmpegのインストールに辿り着ける。
$ brew install ffmpeg

#   エラーが出る場合は以下も試す

$ brew install --use-clang --HEAD ffmpeg
動画変換(ffmpeg
  • ffmpegが使えるようになったら、デスクトップのworkフォルダに移動して、動画にしてみた。
$ cd ~/Desktop/work
$ ffmpeg -r 3 -i %03d.png -vcodec mjpeg -sameq -y ./out.avi

できた、できた!画面の変化の様子がパラパラアニメになっている!

  • rオプションを変更すれば、1秒間に再生する画像の枚数を指定できる。
    • 上記の例では秒間3枚。
    • rオプションなしならデフォルト値(=24枚?)
  • 上記の形式はモーションJPEGなので動画サイズが巨大になりがち。(1.1MB)
    • JPEGのパラパラアニメなので、JPEG画像×枚数分のサイズなってしまう。
  • そこで、再びffmpegを使って、今度はmpeg4に変換してみる。(213KB)
$ ffmpeg -i ./out.avi -vcodec libx264 -f mp4 -y out.mp4
動画変換(Quicktime Pro)

デスクトップのインターバル撮影

以上の実験で理解した仕組みを可能な限り自動化してみる。

撮影&縮小&ファイル数制限
  • 定期的に自分でcommand-shift-3を押すなんてやってられないので、5秒間隔で自動撮影するようにした。
    • screencaptureコマンドを使えば、シャッター音なしで静かに実現できる。
    • デスクトップにcaptureフォルダを作って、そこに保存しておく。
  • 撮影したらすぐに、画像サイズを960pxに縮小しておく。
  • また、無制限に画像ファイルが増えてしまっても困るので、最新の100ファイルのみ保持するようにした。
      • ~/Desktop/cap.bash
#!/bin/bash
# デスクトップを撮影して、最新の100枚だけ保持する

fdir="$HOME/Desktop/capture"
fname="`date +%s`.png"
[ -e $fdir ] || mkdir $fdir
cd $fdir
screencapture -xC $fname 2>/dev/null
sips -Z 960 $fname

limit=100
fnum=`ls|wc -l`
over=`expr $fnum - $limit`
rm `ls 2>/dev/null | head -n$over`
  • ターミナルで実行権限を追加しておいた。
$ chmod a+x ~/Desktop/cap.bash
launchdの設定
  • そしてlaunchdによって、上記シェルスクリプトを5秒間隔で実行するようにすれば良いのだ。
      • ~/Desktop/com.zarigani.DesktopCapture.plist
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
	<key>Label</key>
	<string>com.zarigani.DesktopCapture</string>
	<key>ProgramArguments</key>
	<array>
		<string>/Users/zari/Desktop/cap.bash</string>
	</array>
	<key>StartInterval</key>
	<integer>5</integer>
</dict>
</plist>
  • launchdの設定ファイルはxmlであり、慣れないと訳が分からないが、Lingon.appを使えば簡単に設定できる。

20121127112234

  • 5秒間隔で、デスクトップに置いたcap.bashを実行する設定である。
  • ~/Library/LaunchAgents/com.zarigani.DesktopCapture.plist に置いておくと、ログイン時に自動実行される。
  • しかしまだテスト段階なので、~/Desktop/com.zarigani.DesktopCapture.plist に移動しておいた。
launchdによる起動と終了
  • launchdに設定ファイルをloadすれば、設定したとおりに5秒間隔で撮影を始める。
$ launchctl load ~/Desktop/com.zarigani.DesktopCapture.plist
  • unloadすれば、5秒間隔の撮影を中止する。
$ launchctl unload ~/Desktop/com.zarigani.DesktopCapture.plist

動画に変換する

  • そして、デスクトップのcaptureフォルダに画像がたまったら、以下のシェルスクリプトで動画に変換するのだ。
  • 動画にするには事前にファイル名が数字の連番になっている必要がある。
    • さすがに100ファイルを連番にリネームするのは、手作業ではやってられない...。
  • その作業をするために、一時フォルダにcaptureフォルダをコピーして、
  • そこで、0001.png 0002.png ...というファイル名に変更している。
  • ファイル名が連番になったら、上記実験の要領で動画としてデスクトップに出力している。
      • ~/Desktop/mov.bash
#!/bin/bash
# 静止画から動画を作成する

fdir="$HOME/Desktop/capture"
wdir="${TMPDIR}TemporaryItems"
[ -e $wdir ] || mkdir $wdir
cp -r $fdir $wdir
cd "$wdir/`basename $fdir`"

# [ヅ] AWK でファイルに連番を振って一括リネームするワンライナー (2011-10-09)
# http://www.nilab.info/z3/20111009_05.html
ls *.png|awk '{ printf "mv %s %04d.png\n", $0, NR }'|sh

# ffmpeg静止画→動画作成苦戦しまくり - ヰタ・デテスタビリス (reuniの研究日記)
# http://d.hatena.ne.jp/reuni/20080131/1201771541
#ffmpeg -i %04d.png -y $fdir.mpg

# インターバル撮影/インターバル撮影した静止画から動画作成 - matoken's wiki.
# http://hpv.cc/~maty/pukiwiki1/index.php?%A5%A4%A5%F3%A5%BF%A1%BC%A5%D0%A5%EB%BB%A3%B1%C6%2F%A5%A4%A5%F3%A5%BF%A1%BC%A5%D0%A5%EB%BB%A3%B1%C6%A4%B7%A4%BF%C0%C5%BB%DF%B2%E8%A4%AB%A4%E9%C6%B0%B2%E8%BA%EE%C0%AE
ffmpeg -r 3 -i %04d.png -vcodec mjpeg -sameq -y ./out.avi
ffmpeg -i ./out.avi -vcodec libx264 -f mp4 -y $fdir.mp4

rm -fr "$wdir/`basename $fdir`"
qlmanage -p $fdir.mp4
  • 当初、ファイル名を連番にするために、forループを使って一生懸命やろうとしていたが、
  • awkを使えばワンライナーで素早く完了してしまうことを知って驚愕!(素晴らしい情報に感謝です!)
    • よって、1行目を処理中の場合、awkの処理式は以下のような状態になっている。
    • awk '{ printf "mv %s %04d.png\n", "XXXXXXXX.png", 1 }'」
    • awkはprintf書式に従って、「mv XXXXXXXX.png 0001.png」を出力する。
    • そして「mv XXXXXXXX.png 0001.png」をshにパイプで渡すと、コマンドとして実行されるのだ。
    • つまり、lsで出力されたファイル名を、順番(デフォルトでファイル名順になっているはず)に0001.png、0002.pngと連番にすることになるのだ!

ループがワンライナーになってしまうawkコマンドって素晴らしい!

  • その後は、実験した時と同じようにffmpegコマンドを実行して、動画を生成している。
  • 最後に、不要になった一時フォルダのcaptureを削除して、
  • 生成された動画をクイックルックで表示している。
$ chmod a+x ~/Desktop/mov.bash

AppleScriptにまとめる

  • 「launchctl load ~/Desktop/com.zarigani.DesktopCapture.plist」で5秒間隔の連続撮影を始めて、
  • 「launchctl unload ~/Desktop/com.zarigani.DesktopCapture.plist」で連続撮影の中止。
  • 動画を見るには、~/Desktop/mov.bashを実行する。
  • 以上はシンプルなコマンドだけど、いずれ忘れる...。(1ヶ月後には忘れている自信がある)
  • だから、AppleScriptに組み込んで、起動中は連続撮影して、終了したら連続撮影を中止するようにしてみる。
  • それから、ドラッグ&ドロップで静止画を動画に変換して、プレビューできるようにするのも良さそう。


--起動したら、5秒間隔で連続撮影する
on run
do shell script "launchctl load " & launchd_plist_path() end run

--終了したら、連続撮影をやめる
on quit
do shell script "launchctl unload " & launchd_plist_path() continue quit --quitハンドラではなく、アプリケーションのquitを実行する
end quit

--フォルダをドラッグ&dロップした時の処理
on open drop_items
try
--1つだけかどうか?--複数ではエラーにする
if drop_items's number > 1 then error
--フォルダかどうか?--フォルダでなければエラーになる
tell application "Finder" to folder (drop_items's item 1 as text) set fdir to (drop_items's item 1)'s POSIX path
do shell script mov_bash_path() & space & fdir
on error
"画像の入ったフォルダを1つだけドラッグ&ドロップしてください。"
display dialog result buttons "OK" default button 1 with icon 0
end try
--5秒間隔で連続撮影していない時は即終了する
if not exists_DesktopCapture() then continue quit
end open




on launchd_plist_path() (path to resource "com.zarigani.DesktopCapture.plist")'s POSIX path
end launchd_plist_path

on mov_bash_path() (path to resource "mov.bash")'s POSIX path
end mov_bash_path

on exists_DesktopCapture() try
do shell script "launchctl list | grep com.zarigani.DesktopCapture"
true
on error
false
end try
end exists_DesktopCapture

  • 上記AppleScriptを以下の形式で保存しておいた。
    • ファイル名 = 「DesktopLogger」
    • ファイルフォーマット = アプリケーション
    • オプション =「実行後、自動的に終了しない」チェックあり

20121128085046

  • そして、デスクトップにあるシェルスクリプト・launchdの設定ファイルは、すべて上記DesktopLoggerのResourcesフォルダに入れておくのだ。
    • ~/Desktop/com.zarigani.DesktopCapture.plist
    • ~/Desktop/cap.bash
    • ~/Desktop/mov.bash(↓若干の修正を加えた↓)

20121128084549

      • ~/Desktop/mov.bash
#!/bin/bash
# 静止画から動画を作成する

# "${1%/}"=パス末尾の/を取り除く
# 例: /a/b/c/ -> /a/b/c
fdir="${1%/}"
wdir="${TMPDIR}TemporaryItems"
[ -e $wdir ] || mkdir $wdir
cp -r $fdir $wdir
cd "$wdir/`basename $fdir`"

# AWKでファイルに連番を振って一括リネームするワンライナー
ls *.png|awk '{ printf "mv %s %04d.png
", $0, NR }'|sh

# 静止画から動画作成 & MP4変換
/usr/local/bin/ffmpeg -r 3 -i %04d.png -vcodec mjpeg -sameq -y ./out.avi
/usr/local/bin/ffmpeg -i ./out.avi -vcodec libx264 -f mp4 -y $fdir.mp4

rm -fr "$wdir/`basename $fdir`"
qlmanage -p $fdir.mp4
  • DesktopLoggerを起動すると5秒間隔の連続撮影が始まり、終了すると連続撮影は停止する。
  • DesktopLoggerにcaptureフォルダをドラッグ&ドロップすると、その中の静止画が動画に変換される。

これで、1か月後にすべてを忘れても使えるアプリケーションになった!

追記1

  • デスクトップに置いた実験用の~/Desktop/com.zarigani.DesktopCapture.plistに依存してしまう状態になっていたので、
  • アプリケーションバンドル内のcom.zarigani.DesktopCapture.plistを使うように修正しました。


--起動したら、5秒間隔で連続撮影する
on run
do shell script "defaults write " & launchd_plist_path() & " StartInterval -int 5"
do shell script "defaults write " & launchd_plist_path() & " ProgramArguments -array " & cap_bash_path() do shell script "launchctl load " & launchd_plist_path() end run

--終了したら、連続撮影をやめる
on quit
do shell script "launchctl unload " & launchd_plist_path() continue quit --quitハンドラではなく、アプリケーションのquitを実行する
end quit

--フォルダをドラッグ&dロップした時の処理
on open drop_items
try
--1つだけかどうか?--複数ではエラーにする
if drop_items's number > 1 then error
--フォルダかどうか?--フォルダでなければエラーになる
tell application "Finder" to folder (drop_items's item 1 as text) set fdir to (drop_items's item 1)'s POSIX path
do shell script mov_bash_path() & space & fdir
on error
"画像の入ったフォルダを1つだけドラッグ&ドロップしてください。"
display dialog result buttons "OK" default button 1 with icon 0
end try
--5秒間隔で連続撮影していない時は即終了する
if not exists_DesktopCapture() then continue quit
end open




on launchd_plist_path() (path to resource "com.zarigani.DesktopCapture.plist")'s POSIX path
end launchd_plist_path

on cap_bash_path() (path to resource "cap.bash")'s POSIX path
end cap_bash_path

on mov_bash_path() (path to resource "mov.bash")'s POSIX path
end mov_bash_path

on exists_DesktopCapture() try
do shell script "launchctl list | grep com.zarigani.DesktopCapture"
true
on error
false
end try
end exists_DesktopCapture

追記2

  • デスクトップにファイルを保存すると、常にTimeMachineでバックアップの対象となってしまい無駄が多いので、
  • デフォルトの保存場所を一時フォルダに変更した。(AppleScriptのpath to temporary items)
  • launchdで繰り返しの秒間を10秒以下に設定しても、実際には10秒以上の繰り返し間隔になってしまうことが判明。(自分の環境では)
  • 指定した正確な秒数間隔で実行するため、AppleScriptのon idleハンドラを利用する方式に書き換えた。
  • シェルスクリプトの実行は、バックグラウンドジョブとして実行するようにした。
  • ビットレートなどの関係か、モーションJPEGをMP4に変換するときエラーが発生してしまうことがあるので、MP4変換はやめた。
      • DesktopLogger_on_idle.app


property interval : 5 --5秒間隔で撮影する
property pxwh : 960 --画像サイズを960px以内に縮小する
property limit : 100 --最新の100枚のスクリーンショットを保持する
property fps : 8 --1秒間に8コマ再生する

--5秒ごとに繰り返す
on idle
--" >& /dev/null &" バックグラウンド処理で実行するため
do shell script cap_bash_path() & space & capture_folder() & space & pxwh & space & limit & " >& /dev/null &"
return interval
end idle

--フォルダをドラッグ&ドロップした時の処理
on open drop_items
try
--1つだけかどうか?--複数ではエラーにする
if drop_items's number > 1 then error
--フォルダかどうか?--フォルダでなければエラーになる
tell application "Finder" to folder (drop_items's item 1 as text) set fdir to (drop_items's item 1)'s POSIX path
do shell script mov_bash_path() & space & fdir & space & fps
on error
"画像の入ったフォルダを1つだけドラッグ&ドロップしてください。"
display dialog result buttons "OK" default button 1 with icon 0
end try
--5秒間隔で連続撮影していない時は即終了する
--if not exists_DesktopCapture() then continue quit
end open

--Dockアイコンをクリックした時の処理
on reopen
{"キャンセル", "画像フォルダを開く", "動画を見る"} set res to display dialog "" buttons result default button 3 with title (my name as text) if res's button returned is "画像フォルダを開く" then
tell application "Finder" to open (my capture_folder() as POSIX file) else if res's button returned is "動画を見る" then
do shell script mov_bash_path() & space & capture_folder() & space & fps
end if
end reopen




on launchd_plist_path() (path to resource "com.zarigani.DesktopCapture.plist")'s POSIX path
end launchd_plist_path

on cap_bash_path() (path to resource "cap.bash")'s POSIX path
end cap_bash_path

on mov_bash_path() (path to resource "mov.bash")'s POSIX path
end mov_bash_path

on exists_DesktopCapture() try
do shell script "launchctl list | grep com.zarigani.DesktopCapture"
true
on error
false
end try
end exists_DesktopCapture

on capture_folder() (path to temporary items)'s POSIX path & "DesktopLogger_capture"
end capture_folder

#!/bin/bash
# デスクトップを撮影して、最新の100枚だけ保持する
# cap.bash fdir pxwh limit

fdir="${1:-${TMPDIR}TemporaryItems/DesktopLogger_capture}"
fname="`date +%s`.png"
[ -e "$fdir" ] || mkdir "$fdir"
cd "$fdir"
screencapture -xC $fname 2>/dev/null

pxwh=${2:-960}
sips -Z $pxwh $fname

limit=${3:-100}
fnum=`ls|wc -l`
over=`expr $fnum - $limit`
rm `ls 2>/dev/null | head -n$over`
#!/bin/bash
# 静止画から動画を作成する
# mov.bash fdir fps

# ${変数名:-"abc"} = 変数に値が設定されていなければ"abc"を代入する
# ${変数名%/} = パス末尾の/を取り除く(例: /a/b/c/ -> /a/b/c )
fdir="${1:-${TMPDIR}TemporaryItems/DesktopLogger_capture}"
fdir="${fdir%/}"
wdir=${TMPDIR}TemporaryItems/DesktopLogger_tmp
rm -fr $wdir
mkdir $wdir
cp -r "$fdir"/* $wdir
cd $wdir

# AWKでファイルに連番を振って一括リネームするワンライナー
ls *.png|awk '{ printf "mv %s %04d.png
", $0, NR }'|sh

# 静止画から動画作成 & MP4変換
fps=${2:-4}
/usr/local/bin/ffmpeg -r $fps -i %04d.png -vcodec mjpeg -sameq -y "$fdir.avi"
#ビットレートなどの関係でmp4に変換できなくなることがあるのでコメントアウト
#/usr/local/bin/ffmpeg -i $fdir.avi -vcodec libx264 -f mp4 -y $fdir.mp4

rm -fr $wdir
qlmanage -p "$fdir.avi"