Google Colaboratory (以下Colab)*1は、Googleが提供するJupyterノートブック環境だ。Google Driveと連携することで、そこに保存されたファイルをColab上で加工することもできる。
この投稿では、Google Driveに保存した動画ファイル(MP4ファイル)を加工し、最終的にGIFアニメーションとして出力する手順を紹介する。GIFを出力するにあたって、MP4を次のように加工する。
- 画面からGIF出力対象部位をトリミング(クロップ)する。
- 画面サイズをオリジナルから縮小する。
- オリジナルのフレームレートを間引く。
加工に際し、MoviePyを使用する場合と、OpenCV + Pillowを用いる場合での処理パフォーマンスも比較した。MoviePyを使用した書式は分かりやすいのだが、OpenCVの処理パフォーマンスは圧倒的に高速だった。
Google Driveのマウント
Google Driveのマウントについて、Colabは様々な方法をサポートしている*2。注意したいのは、マウント後のパスだ。以下に示すのはColabのディレクトリ構造だ。
🔎Colabのディレクトリ構造
"/content"にGoogle Driveが"/drive"としてマウントされているのが分かる。Colab上で、実際に次のLinuxコマンドを実行しても確認することができる。
これはColab環境のGUIを介してマウントした場合だ。
!pwd !ls
もしPythonプログラムから手動でマウントした場合、マウント・パスの"/drive"が、プログラムで指定した通りに変化する。例えば、次の場合は"/gdrive"に変化する。ディレクトリ構造も変化しているのが確認できる。
!pwd from google.colab import drive drive.mount('./gdrive')
事前準備、前提条件
この投稿で加工する動画ファイル(MP4ファイル)は、先日の投稿末尾に添付したTwitter投稿*3に含まれるものを使用している。
ColabのGUIを介してGoogle Driveをマウントした。このMP4ファイルを、Google Drive上の次のパスへ保存している。
/content/drive/MyDrive/20230404/screen-20230331-142030.mp4
この動画の上半分、ちょうどコンソールが表示されている部分をGIFアニメーションとして出力する。出力に際し、フレームレートを12fpsへ落とすことにした。
動画のサイズ
width | height | |
---|---|---|
MP4のサイズ | 1800 | 2784 |
トリミングしたサイズ cropしたサイズ |
1800 | 1392 |
GIFのサイズ resizeしたサイズ |
640 | 495 |
下記Pythonプログラム中には、これらの情報が暗黙に反映されていることに注意すること。
MP4からGIFへの変換
理屈と仕組み。
MP4からGIFへ変換するに際し、動画を構成する一連の画像(フレーム)を取り出し、一つずつ変換していくのが、単純かつ原理的な仕組みだ。変換処理において、変換対象となる画像が小さいほど効率が良いため、対象が小さくなる処理を優先的に実行する。ここでは
- トリミング(crop)
- 画像縮小(resize)
という順序で対応することになる。フレームレートを落とすことによって、さらに変換対象となる画像の枚数を減らすことができる。
OpenCV + Pillowの場合
まずは変換処理にOpenCV (CV: Computer Vision)*4とPillow (PIL: Python Imaging Library)を用いる。どちらも画像処理ライブラリだ。
関数getCroppedFramesでGIF変換対象となる一連の画像を、Pillow imageの配列として出力する。ここではOpenCVを利用している。この仕様について一つ注意しておきたいが、次の特性だ。
つまり、デフォルトのカラー・フォーマットがBGRであることから、RGBへ変換する必要がある。
Note that the default color format in OpenCV is often referred to as RGB but it is actually BGR (the bytes are reversed).
OpenCV: Color Space Conversions
def getCroppedFrames(movie, crop_size, resize_size): ret_images = [] while True: ret, bgr_images = movie.read() if ret: cropped_images = bgr_images[0:crop_size["h"], 0:crop_size["w"]] resized_images = cv2.resize(cropped_images, (resize_size["w"], resize_size["h"])) rgb_images = cv2.cvtColor(resized_images, cv2.COLOR_BGR2RGB) pillow_images = Image.fromarray(rgb_images) ret_images.append(pillow_images) else: return ret_images
次に関数makeGIFでGIFファイルを出力する。ここではFPSを指定することで、変換枚数を間引いている。出力形式は指定されたパスに従って特定されるため、あえて関数のパラメータとして画像形式を指定する必要はない。
def makeGIF(path, images, fps): dur = int(1000.0 / fps) images[0].save(path, save_all=True, append_images=images[1:], duration=dur, loop=0)
一連のコードは次のようになる。約7~8秒で変換処理を終えているのが分かる。
MoviePyの場合
MoviePy*5は動画編集用のライブラリだ。OpenCVに比べて、書式が非常に分かりやすい。おそらく目的に応じて「このように書きたい」という考えそのままに書き下すことができ、結果として非常に分かりやすいプログラムが出来上がる。たとえば、このような具合だ。
input_video = VideoFileClip(source_path) cropped_video = input_video.crop(x1=0, y1=0, width=1800, height=1392) resized_video = cropped_video.resize(height=640) resized_video.write_gif(target_path_ffmpeg, fps=12, program='ffmpeg', logger='bar')
OpenCVの場合と構成を合わせて書き換えると、次のようになる。
関数getCroppedFrames
def getCroppedFrames(movie, crop_size, resize_size): cropped_movie = movie.crop(x1=0, y1=0, width=crop_size["w"], height=crop_size["h"]) resized_movie = cropped_movie.resize(height=resize_size["h"]) return resized_movie
関数makeGIF
def makeGIF(path, movie, fps, program): movie.write_gif(path, fps=fps, program=program, logger='bar')
しかし、これが遅いのだ。たとえ分かりやすくプログラムを書けたとしても、同じ変換処理に20倍以上の時間を要するとしたらどうだろう。一つのGIFを出力するのに3分超を要するのだ。
GIF変換に際し、使用するプログラム(FFmpeg*6、imageio*7など)を選択することができるが、処理時間に違いはない。
2つの変換プログラムを用いて、2種類のGIFを出力すると、通しの処理時間は8分足らず。一連のコードは次のようになる。一つの変換処理に3分程度を要しているのが分かる。
出力されたGIFの比較
最後に、出力されたGIFの比較だ。変換処理プログラムの書式が異なるだけで、その他に大差のないことが分かる。
Pillow 6.10MB |
ffmpeg 4.57MB |
imageio 4.62MB |
---|---|---|