FFmpegで画像から動画を作る(RGB→YUV)と色が異なってしまうときの具体例と対処

オリジナルの写真をPhotoshopで開いたところ。PhotoshopのカラースペースsRGB IEC61966-2.1で統一しています。
なお、縦横比を補正する前のため、ドクターイエローの顔が長く伸びています。

TrainScannerで作成したドクターイエローの編成写真をSNSでシェアするためにPNGファイルから動画ファイルを生成してみたら、VLCなどの動画プレーヤーで見たときに、なんとなく赤っぽい黄色になってしまいました。FFmpegで動画を作ったときに色化けしたようです。

FFmpeg colorspace などのキーワードでネット検索して、FFmpegでRGB画像からYUV形式の動画を作るときに色空間の変換が必要になることが分かりました(詳細はこちら)が、実際にどれくらい違って見えるのか比較してみました。

動作環境と動画データの詳細

動作環境は以下のとおりです。

  • MacBook Pro (16インチ, 2024)
    Liquid Retina XDRディスプレイ搭載、ディスプレイのプリセットはApple XDR Display (P3-1600 nits)を選択している状態
  • macOS Sequoia 15.6.1
    動画プレーヤーのスクリーンショットはOSの機能(⌘+Shift+5)を使ってPNGファイルに保存
  • QuickTime Player バージョン10.5
  • VLC media player Version 3.0.21 Vetinari (Apple Silicon)
  • Photoshop 26.11.0
    Liquid Retina XDRディスプレイに表示している動画プレーヤーのスクリーンショットはPNG形式(カラープロファイルはカラーLCDとなっている)だが、Photoshopで開いたときに編集メニュープロファイル変換を使ってsRGB IEC61966-2.1に変換している
  • ffmpeg version 8.0 built with Apple clang version 17.0.0 (clang-1700.0.13.3)
    Homebrewを使ってインストールしたFFmpegを使用

PNGファイルから作成した動画データは、colorspaceフィルターなしで作った動画とcolorspaceフィルターありで作った動画の2種類を準備しました。

  • colorspaceフィルターなしの場合:
ffmpeg -f rawvideo -pix_fmt rgb24 -i pipe: \
    -pix_fmt yuv420p output.mp4
ffmpeg -f rawvideo -pix_fmt rgb24 -i pipe: \
    -filter_complex "[0]colorspace=bt709:fast=1:iall=bt601-6-625[s0]" -map "[s0]" \
    -color_primaries 1 -color_range 1 -color_trc 1 -colorspace 1 \
    -sws_flags spline+accurate_rnd+full_chroma_int \
    -pix_fmt yuv420p output.mp4

実際にはPythonを使ってPillowでPNGファイルを読み込み、numpyでndarray((高さ, 幅, 3), dtype=np.uint8)に変換し、ndarray.tobytes()で得たRGB画像のバイトデータを-i pipe:で標準入力からFFmpegに送り込んでいます。

colorspaceフィルターあり・なしで再生動画を比較すると

VLC media playerで再生した場合

上から順に元となるPNG画像(Photoshop)、colorspaceフィルターなしの動画、colorspaceフィルターありの動画です。

元々PNG画像の黄色は少し赤みを帯びていましたが、colorspaceフィルターなしの動画は赤みが増して見えます。MacBook Proのディスプレイではあまり気にならなかったのですが、Windows 11 PCで使っている32インチ外部ディスプレイで見たときに赤みが増していることに気付きました。ヒストグラムにおいても赤と緑の山が分離して赤が強くなっています。

colorspaceフィルターありの動画はヒストグラムの赤と緑の山が少し分離しているものの、PNG画像の色合いに近づいて改善できているように思います。

オリジナルのPNG画像データをPhotoshopで表示
オリジナルのPNG画像データをPhotoshopで表示
colorspaceフィルターなしの動画をVLCで再生した場合
colorspaceフィルターなしの動画をVLCで再生した場合
colorspaceフィルターありの動画をVLCで再生した場合
colorspaceフィルターありの動画をVLCで再生した場合

VLC media player(macOS)の表示設定を変えて再生した場合

macOS用VLC media playerの場合、設定画面→すべてを表示ビデオMac OS Xを開くとColorspace conversionという設定項目があります。

Display primariesの初期状態はUnknown primariesが選択されていますが、これをAdobe RGB (1998)に変更してVLCを起動しなおすと、オリジナルのPNG画像に近い色合いで表示されるようになりました。他の設定値も試してみましたが赤または緑がかった色合いになってしまうので、今回の動画データでは少なくともAdobe RGB (1998)が一番良い感じです。

macOS版VLC media playerのcolorspace設定
macOS版VLC media playerのcolorspace設定
colorspaceフィルターありの動画をVLC (Adobe RGB (1998))で再生した場合
colorspaceフィルターありの動画をVLC (Adobe RGB (1998))で再生した場合

QuickTime Playerで再生した場合

次はQuickTime Playerで動画を再生した場合です。上から順にcolorspaceフィルターなしの動画、colorspaceフィルターありの動画です。

colorspaceフィルターなしの動画をQuickTimeで再生した場合
colorspaceフィルターなしの動画をQuickTimeで再生した場合
colorspaceフィルターありの動画をQuickTimeで再生した場合
colorspaceフィルターありの動画をQuickTimeで再生した場合

QuickTime Playerで再生した場合はcolorspaceフィルターなしの動画でも赤みが気にならないように思いました。ヒストグラムはcolorspaceフィルターありの動画をVLCで再生した場合に近い感じです。

colorspaceフィルターありの動画はPNG画像(Photoshop)のヒストグラムとほぼ同じで、見た目もPNG画像とほぼ同じような感じになりました。Colorspace support in FFmpegで示されているcolorspaceフィルターが効いているのが分かります。

Pythonで動画を作るときに便利なパッケージ

ffmpeg-python

Pythonで動画を生成するスクリプトを作るときにとても便利なのが ffmpeg-python というパッケージです。

FFmpegの-filter_complexパラメータはとても複雑なフィルターグラフを作って高度な動画処理を行うことができますが、コマンドラインでそれを手作業で作るのはとても難しいと思います。

しかし、ffmpeg-pythonを使えば複雑なフィルターグラフもメソッドチェーンの形で読みやすく記述できます。GitHubのExamplesに分かりやすい例が多数掲載されています。

上で示したcolorspaceフィルターありのコマンドラインはffmpeg-pythonを使って生成しています。

# FFmpegの入力・フィルター・出力をメソッドチェーンで定義する
command = (
    ffmpeg
    .input('pipe:', format='rawvideo', pix_fmt='rgb24', r=fps, s=f'{video_width}x{video_height}')
    .filter_('colorspace', 'bt709', iall='bt601-6-625', fast='1')
    .output('output.mp4', 
        sws_flags='spline+accurate_rnd+full_chroma_int', 
        color_range=1, colorspace=1, color_primaries=1, color_trc=1, 
        pix_fmt=output_pix_fmt, video_bitrate=bitrate, qmin=qmin, qmax=qmax)
    .overwrite_output()
    .compile()
)
# print(command)で生成されたコマンドラインを表示できる

# subprocessでFFmpegを実行する
process = subprocess.Popen(
            command, 
            stdin=subprocess.PIPE, 
            stdout=subprocess.DEVNULL, 
            stderr=subprocess.DEVNULL
        )

fffio

ffmpeg-pythonはFFmpegの機能を網羅的に使うことができて非常に強力なPythonパッケージなのですが、シンプルに動画データの読み書きだけで使おうと思うと、上記のサンプルスクリプトは手間がかかります。

そこで、テキストファイルの読み書きと同じくらい簡単に動画データを読み書きできる fffio というPythonパッケージを作りました。pipを使ってインストールできます。

pip install fffio

動画データからフレームを読み出す場合は、以下のように書くことができます。

from fffio import FrameReader
import cv2

with FrameReader('sample.mp4') as reader:
    for i, frame in enumerate(reader.frames(), 1):
        # frame is a numpy.ndarray(shape=(height, width, 3), dtype=np.uint8).
        _ = cv2.imwrite(
            f'sample{i:05d}.jpg', 
            cv2.cvtColor(frame, cv2.COLOR_RGB2BGR)
        )

画像を動画データに書き込む場合は、以下のように書くことができます。

from fffio import FrameWriter
import cv2
from pathlib import Path

size=(1920, 1080)
with FrameWriter('sample.mp4', size=size) as writer:
    for file in sorted(Path('.').glob('*.jpg')):
        frame = cv2.cvtColor(cv2.imread(str(file)), cv2.COLOR_BGR2RGB)
        frame = cv2.resize(frame, size, interpolation=cv2.INTER_LANCZOS4)
        writer.write(frame)

FrameWriterの中ではFFmpegのcolorspaceフィルターを実装しているので、色合いを心配しないで動画を作れるようになっています。colorspaceフィルターを外す場合はFrameWriterのパラメータにcolorspace=Falseを指定すればOKです。

タイトルとURLをコピーしました