こしあん
2022-12-05

ffmpeg-pythonでサクッとNumPy配列から動画を出力する


7.5k{icon} {views}

ffmpeg-pythonを使って、NumPy配列から動画を生成する方法を説明します。OpenCVのVideoWriterよりも、コーデックの問題が発生しづらかったり、画質を柔軟に選択できるようになります。とりあえず「サクッと」動画を出したいときに使えるのではないかと思います。

はじめに

OpenCVのVideoWriterによる動画エンコードは何かと使いがちですが、不満点もかなりあります。例えば、

  • エンコードでハマりがち
    • 例:コーデックをmp4vで指定すると、Chromeで再生できない動画が出来上がる
    • 例:H264でエンコードしようとすると、かなり環境構築を頑張らないといけない(主にDockerfile)。サクッと動画吐き出したいときにこれをやるとオーバーキル感ある
  • ビットレートを指定できない

OpenCVの動画吐き出しも結局はffmpegからのラッパーなので、ffmpegからやってみようという趣旨です。検索してみたところ、OpenCVの動画の吐き出しは結構出てきましたが、ffmpegからの情報はあまり出てこなかったので、自分用メモとして書いておきます。

Windowsの場合は、実行ディレクトリにffmpegのバイナリおくorバイナリにパスを通すということがいるので、どちらかというとLinux環境での想定です。このケースでは、Docker上で動画を吐き出すケースを想定しています。

ベースのイメージ

ffmpegとffmpeg-pythonと最低限のPythonが動くだけのDockerイメージを作ります。

FROM ubuntu:20.04

RUN apt-get update
ENV TZ=Asia/Tokyo
RUN ln -snf /usr/share/zoneinfo/$TZ /etc/localtime && echo $TZ > /etc/timezone
RUN apt-get install -yq --no-install-recommends python3-pip \
        python3-dev \
        wget \
        git  \
        ffmpeg \
        tzdata && apt-get upgrade -y && apt-get clean

RUN ln -s /usr/bin/python3 /usr/bin/python

RUN pip install -U pip &&\
  pip install --no-cache-dir ffmpeg-python pillow numpy

ビルドして実行します。

docker build -t ffmpeg_numpy .
docker run --rm -it -v $(pwd)/src:/src ffmpeg_numpy

カレントディレクトリ以下の「src」フォルダを作業ディレクトリとし、マウントさせます。

Pythonファイルを実行するときは、

root@f4bc2c33f365:/# cd src
root@f4bc2c33f365:/src# ls
cat.jpg  ffmpeg_numpy.py  out.mp4
root@f4bc2c33f365:/src# python ffmpeg_numpy.py

このように実行します。公式ドキュメントが参考になりました。

https://kkroening.github.io/ffmpeg-python/

NumPy配列から動画を作る

まず単純な例として、

  • Inputの動画はなし
  • フレームを何らかのNumPy配列で作り
  • Outputは動画

とします。適当な画像をスライドさせる動画を作ってみましょう。以下の画像を「cat.jpg」とします。

import ffmpeg
from PIL import Image
import numpy as np

def write_test():
    base_img = np.array(Image.open("cat.jpg"))
    height, width = base_img.shape[:2]

    process = (
        ffmpeg
        .input('pipe:', format='rawvideo', pix_fmt='rgb24', s='{}x{}'.format(width-250, height))
        .output("out.mp4", pix_fmt='yuv420p')
        .overwrite_output()
        .run_async(pipe_stdin=True)
    )
    for i in range(250):
        frame = base_img[:, 250-i:width-i, :]
        process.stdin.write(frame.tobytes())
    process.stdin.close()
    process.wait()

if __name__ == "__main__":
    write_test()

以下のような動画が吐き出されます。

フレームレートを変更する

先程出力されたビデオは25fpsでした。任意のFPSに変更したいと思います。

ffmpegのフレームレートの設定は「-r」オプションでできるので、

def fps_test():
    process = (
        ffmpeg
        .input('pipe:', format='rawvideo', pix_fmt='rgb24', s='{}x{}'.format(640, 320), r=5)
        .output("out.mp4", pix_fmt='yuv420p')
        .overwrite_output()
        .run_async(pipe_stdin=True)
    )
    for i in range(256):
        frame = np.full((320, 640, 3), 255, np.uint8)
        frame[:,:,1] = 255-i
        frame[:,:,2] = i
        process.stdin.write(frame.tobytes())
    process.stdin.close()
    process.wait()

このようにすると5fpsの動画ができます。256フレームが5fpsなので、51秒です。

動画から動画

より実践的な例として、Inputが動画、Outputが別の動画を見ていきます。一本のprocessとしても書けますが、複雑な処理をすることを考えて、フレーム単位で一旦NumPy配列として引き出します。

このやり方は公式ドキュメントにも載っており、これをほぼコピペしました。

以下のコードでは次のことをしています。

  • 入力動画の解像度(1920×1080)→半分に圧縮して動画出力
  • FPSはそのまま
  • 画素値を0.4倍にして暗くする
def video_to_video():
    probe = ffmpeg.probe("target_video.mp4")
    video_streams = [stream for stream in probe["streams"] if stream["codec_type"] == "video"]
    width = video_streams[0]["width"]
    height = video_streams[0]["height"]
    fps = int(eval(video_streams[0]["r_frame_rate"]))

    cap = (
        ffmpeg
        .input("target_video.mp4")
        .output('pipe:', format='rawvideo', pix_fmt='rgb24')
        .run_async(pipe_stdout=True)
    )

    writer = (
        ffmpeg
        .input('pipe:', format='rawvideo', pix_fmt='rgb24', s='{}x{}'.format(width//2, height//2), r=fps)
        .output("output.mp4", pix_fmt='yuv420p')
        .overwrite_output()
        .run_async(pipe_stdin=True)
    )

    while True:
        in_bytes = cap.stdout.read(width * height * 3)
        if not in_bytes:
            break
        in_frame = (
            np
            .frombuffer(in_bytes, np.uint8)
            .reshape([height, width, 3])
        )

        out_frame = Image.fromarray(in_frame).resize((width//2, height//2), Image.Resampling.BILINEAR)
        out_frame = np.array(out_frame) * 0.4

        writer.stdin.write(
            out_frame
            .astype(np.uint8)
            .tobytes()
        )

    writer.stdin.close()
    cap.wait()
    writer.wait()

結果をプロットすると次のようになります。

画質を変更する

ビットレート(画質)を明示的に調整できるのが、ffmpegの強みだと思います。このやり方はissueにありました

outputの部分で、

    writer = (
        ffmpeg
        .input('pipe:', format='rawvideo', pix_fmt='rgb24', s='{}x{}'.format(width//2, height//2), r=fps)
        .output("output.mp4", pix_fmt='yuv420p', **{'b:v': '3000k'})
        .overwrite_output()
        .run_async(pipe_stdin=True)
    )

このようにするとビットレートを設定できます。この例では、3000kbpsを目標にエンコードしています。実際にエンコード結果を見ると、

上は映像ビットレートをなにも指定しなかったケース(先程の例)、下は映像ビットレートを3000kに指定したケースです。見ての通り、ビットレートが930kbps→2983kbpsに上がっています。

ffmpegのログを消したい

「quiet=True」を入れます

        .run_async(pipe_stdout=True, quiet=True)

ライセンスについて

ffmpegというとライセンスが気になることもあると思います。「apt-get install ffmpeg」からインストールした場合、以下のようなライセンスになっていました。

ffmpeg -L
root@f4bc2c33f365:/src# ffmpeg -L
ffmpeg version 4.2.7-0ubuntu0.1 Copyright (c) 2000-2022 the FFmpeg developers
  built with gcc 9 (Ubuntu 9.4.0-1ubuntu1~20.04.1)
  configuration: --prefix=/usr --extra-version=0ubuntu0.1 --toolchain=hardened --libdir=/usr/lib/x86_64-linux-gnu --incdir=/usr/include/x86_64-linux-gnu --arch=amd64 --enable-gpl --disable-stripping --enable-avresample --disable-filter=resample --enable-avisynth --enable-gnutls --enable-ladspa --enable-libaom --enable-libass --enable-libbluray --enable-libbs2b --enable-libcaca --enable-libcdio --enable-libcodec2 --enable-libflite --enable-libfontconfig --enable-libfreetype --enable-libfribidi --enable-libgme --enable-libgsm --enable-libjack --enable-libmp3lame --enable-libmysofa --enable-libopenjpeg --enable-libopenmpt --enable-libopus --enable-libpulse --enable-librsvg --enable-librubberband --enable-libshine --enable-libsnappy --enable-libsoxr --enable-libspeex --enable-libssh --enable-libtheora --enable-libtwolame --enable-libvidstab --enable-libvorbis --enable-libvpx --enable-libwavpack --enable-libwebp --enable-libx265 --enable-libxml2 --enable-libxvid --enable-libzmq --enable-libzvbi --enable-lv2 --enable-omx --enable-openal --enable-opencl --enable-opengl --enable-sdl2 --enable-libdc1394 --enable-libdrm --enable-libiec61883 --enable-nvenc --enable-chromaprint --enable-frei0r --enable-libx264 --enable-shared
  libavutil      56. 31.100 / 56. 31.100
  libavcodec     58. 54.100 / 58. 54.100
  libavformat    58. 29.100 / 58. 29.100
  libavdevice    58.  8.100 / 58.  8.100
  libavfilter     7. 57.100 /  7. 57.100
  libavresample   4.  0.  0 /  4.  0.  0
  libswscale      5.  5.100 /  5.  5.100
  libswresample   3.  5.100 /  3.  5.100
  libpostproc    55.  5.100 / 55.  5.100
ffmpeg is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation; either version 2 of the License, or
(at your option) any later version.

ffmpeg is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
GNU General Public License for more details.

You should have received a copy of the GNU General Public License
along with ffmpeg; if not, write to the Free Software
Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA

GPLv2でした。少なくとも独自ライセンスは含まれないようです。



Shikoan's ML Blogの中の人が運営しているサークル「じゅ~しぃ~すくりぷと」の本のご案内

技術書コーナー

北海道の駅巡りコーナー


One Comment

Add a Comment

メールアドレスが公開されることはありません。 が付いている欄は必須項目です