iPhone や一眼カメラで撮った HDR 動画を、HDR に対応していない画面(SDR)で再生すると、白っぽく色の薄い映像に見えることがあります。HDR は SDR より、明るさと色の幅が広い形式です。その幅を SDR の範囲に収めないまま表示すると、こう見えます。幅を収める変換をトーンマッピングといいます。FFmpeg では zscale フィルタと tonemap フィルタを組み合わせて行います。
HDR と SDR の違い
| 項目 | SDR(Standard Dynamic Range) | HDR(High Dynamic Range) |
|---|---|---|
| 色空間 | BT.709 | BT.2020 |
| 転送特性 | BT.709 (ガンマ) | PQ (HDR10) / HLG |
| ピクセル深度 | 8bit | 10bit |
| 輝度範囲 | 100 nit | 1000〜10000 nit |
色空間は表せる色の範囲、転送特性は明るさを信号の値に割り当てるルールです。nit は明るさの単位です。
事前確認 — 動画のHDR情報を確認する
変換の前に、入力ファイルが HDR10 か HLG かを調べます。
ffprobe -v quiet -select_streams v:0 \
-show_entries stream=color_space,color_transfer,color_primaries,pix_fmt \
-of default=noprint_wrappers=1 input.mp4
出力例(HDR10):
pix_fmt=yuv420p10le
color_space=bt2020nc
color_transfer=smpte2084
color_primaries=bt2020
出力例(HLG):
pix_fmt=yuv420p10le
color_space=bt2020nc
color_transfer=arib-std-b67
color_primaries=bt2020
見分けるのは color_transfer の値です。smpte2084 なら HDR10(PQ)、arib-std-b67 なら HLG です。
基本コマンド — HDR10 → SDR(BT.709)
zscale を使ったトーンマッピング(推奨)
zscale フィルタを使います。zscale は、libzimg というライブラリを組み込んだ FFmpeg でだけ使えます。
ffmpeg -i input_hdr.mp4 \
-vf "zscale=t=linear:npl=100,format=gbrpf32le,zscale=p=bt709,tonemap=hable,zscale=t=bt709:m=bt709:r=tv,format=yuv420p" \
-c:v libx264 -crf 18 -preset slow \
-c:a copy \
output_sdr.mp4
-vf の中では、左から順に次の処理をしています。
zscale=t=linear:npl=100:PQ の信号を、明るさに比例した値(リニア)に変換する(nplは基準にする明るさ)format=gbrpf32le:計算の精度を保つため、32bit の浮動小数点(小数を扱える形式)にするzscale=p=bt709:色の範囲(原色の位置)を BT.2020 から BT.709 に変えるtonemap=hable:Hable という方式で、明るさの幅を縮めるzscale=t=bt709:m=bt709:r=tv:明るさの割り当て(ガンマ)と色の計算式(matrix)を BT.709 に合わせ、値の範囲を制限レンジ(tv)にするformat=yuv420p:8bit の YUV 4:2:0 にする
-c:a copy は、音声を変換せずにそのまま残す指定です。
トーンマッピングアルゴリズムの比較
tonemap= の値を変えると、明るい部分の縮め方(アルゴリズム)が変わります。下の例は -vf の部分だけです。
# Hable(フィルムルック、推奨)
-vf "zscale=t=linear:npl=100,format=gbrpf32le,zscale=p=bt709,tonemap=hable,zscale=t=bt709:m=bt709:r=tv,format=yuv420p"
# Reinhard(全体の明るさを保つ)
-vf "zscale=t=linear:npl=100,format=gbrpf32le,zscale=p=bt709,tonemap=reinhard,zscale=t=bt709:m=bt709:r=tv,format=yuv420p"
# Mobius(範囲内の色とコントラストを保つ)
-vf "zscale=t=linear:npl=100,format=gbrpf32le,zscale=p=bt709,tonemap=mobius,zscale=t=bt709:m=bt709:r=tv,format=yuv420p"
# Clip(単純クリッピング、最速だが白飛びあり)
-vf "zscale=t=linear:npl=100,format=gbrpf32le,zscale=p=bt709,tonemap=clip,zscale=t=bt709:m=bt709:r=tv,format=yuv420p"
| アルゴリズム | 特徴 | 推奨用途 |
|---|---|---|
| hable | 暗部と明部の細部を reinhard より残す。全体がやや暗くなる | 細部を残したいとき |
| reinhard | 全体の明るさを保つ。細部が平らになり、色の正確さが下がる | 明るさを保ちたいとき |
| mobius | 範囲外の明るさをなめらかに縮め、範囲内の色とコントラストをできるだけ保つ | 色の正確さを優先したいとき |
| clip | 単純カット、高速 | テスト・比較 |
HLG → SDR 変換
ffmpeg -i input_hlg.mp4 \
-vf "zscale=t=linear,format=gbrpf32le,zscale=p=bt709,tonemap=hable,zscale=t=bt709:m=bt709:r=tv,format=yuv420p" \
-c:v libx264 -crf 18 -preset slow \
-c:a copy \
output_sdr.mp4
HDR10 のコマンドとの違いは、最初の zscale に npl=100 がないことだけです。npl を省くと既定値の 100 が使われるので、npl=100 を付けても結果は同じです。
colorspace フィルタは HDR の変換に使えない
colorspace フィルタは、PQ と HLG の転送特性に対応していません。HDR10 の動画に使うと Unsupported input transfer characteristics 16 (smpte2084) というエラーで止まります。iall=bt2020 で入力を BT.2020 と指定すると動きますが、トーンマッピングはされず、くすんだコントラストの低い映像になります。zscale が使えない場合は、libzimg 入りの FFmpeg を入れ直します。
YouTube アップロード向けの設定
YouTube の推奨エンコード設定は、CRF ではなくビットレートで示されています(SDR の 1080p なら、24〜30fps で 8 Mbps、48〜60fps で 12 Mbps)。下の例は CRF 18 で書き出します。
ffmpeg -i input_hdr.mp4 \
-vf "zscale=t=linear:npl=100,format=gbrpf32le,zscale=p=bt709,tonemap=hable,zscale=t=bt709:m=bt709:r=tv,format=yuv420p" \
-c:v libx264 -crf 18 -preset slow \
-pix_fmt yuv420p \
-color_primaries bt709 \
-color_trc bt709 \
-colorspace bt709 \
-c:a aac -b:a 256k \
output_youtube_sdr.mp4
出力には BT.709 であることを示す情報(タグ)が記録され、プレーヤーが色を正しく扱えます。このタグは zscale が変換したときに付きます。-color_primaries・-color_trc・-colorspace の3つも同じ値を指定していますが、FFmpeg 8.1 では、この3つを外しても出力ファイルはまったく同じでした。
一括変換スクリプト(フォルダ内のHDR動画をすべてSDRに変換)
フォルダ内の .mp4 を順に変換し、sdr_元のファイル名 で保存します。HDR ではない動画を通すと、色の情報がない場合は zscale がエラーになって中身が空のファイルが残り、BT.709 の情報がある場合は暗く変換されてしまいます。そのため、ffprobe で color_transfer を調べ、HDR10(smpte2084)と HLG(arib-std-b67)以外は飛ばします。bash で、動画のあるフォルダに移動してから実行します。
#!/bin/bash
for f in *.mp4; do
# HDR10(smpte2084)と HLG(arib-std-b67)以外は飛ばす
if ! ffprobe -v error -select_streams v:0 -show_entries stream=color_transfer \
-of default=noprint_wrappers=1:nokey=1 "$f" | grep -qE 'smpte2084|arib-std-b67'; then
echo "skip: $f"
continue
fi
ffmpeg -i "$f" \
-vf "zscale=t=linear:npl=100,format=gbrpf32le,zscale=p=bt709,tonemap=hable,zscale=t=bt709:m=bt709:r=tv,format=yuv420p" \
-c:v libx264 -crf 18 -preset slow \
-pix_fmt yuv420p \
-color_primaries bt709 -color_trc bt709 -colorspace bt709 \
-c:a copy \
"sdr_${f}"
done
トラブルシューティング
zscale が見つからない
No such filter: 'zscale'
使っている FFmpeg に libzimg が組み込まれていないと、このエラーが出ます。libzimg 入りの FFmpeg を入れ直します。
# macOS(Homebrew の ffmpeg には zimg が入っていないため、ffmpeg-full を入れる)
brew install ffmpeg-full # keg-only なので、brew info ffmpeg-full の案内どおりに PATH に追加する
# Ubuntu
sudo apt install ffmpeg # 最新版を確認
no path between colorspaces で止まる
入力の色の情報(タグ)が unknown になっていて、zscale が何から変換すればよいか分からない状態です。code 3074: no path between colorspaces というエラーで止まり、中身が空の出力ファイルが残ります。ffprobe で color_transfer・color_primaries・color_space を確認してください。color_transfer だけが unknown なら、最初の zscale で入力の転送特性を tin で指定します。HDR10 なら tin=smpte2084、HLG なら tin=arib-std-b67 です(例: zscale=tin=smpte2084:t=linear:npl=100)。t は出力側の指定なので、ここには使いません。color_primaries や color_space も unknown のときは、tin だけでは足りません。フィルタの先頭に setparams を置いて、3つとも指定します(例: setparams=color_primaries=bt2020:color_trc=smpte2084:colorspace=bt2020nc,zscale=t=linear:npl=100,...。HLG なら color_trc=arib-std-b67)。color_transfer の値が間違っているときはエラーにならず、明るさがおかしくなります。これも tin で直せます。
映像が暗すぎる / 白飛びしている
トーンマッピングのアルゴリズムを変えてみます。暗すぎる場合は、npl の値を下げる(例: npl=50)か、reinhard を試します。npl を上げると暗くなるので、白飛びしている場合は npl を上げます(例: npl=203)。
# npl値を下げてより明るいSDRに
-vf "zscale=t=linear:npl=50,format=gbrpf32le,zscale=p=bt709,tonemap=hable,zscale=t=bt709:m=bt709:r=tv,format=yuv420p"
出力が 10bit・4:4:4 になる
フィルタの最後に format=yuv420p を付けないと、エラーにはならず、10bit・4:4:4 の形式(yuv444p10le)が選ばれます。libx264 はそのまま High 4:4:4 Predictive プロファイルでエンコードしますが、QuickTime など多くのプレーヤーは 4:2:0 の H.264 しか再生できません。フィルタの最後には必ず format=yuv420p を付けます。
関連記事
よくある質問
HDR → SDR の最もシンプルなコマンドは?
ffmpeg -i hdr.mp4 -vf "zscale=t=linear:npl=100,format=gbrpf32le,zscale=p=bt709,tonemap=hable,zscale=t=bt709:m=bt709:r=tv,format=yuv420p" -c:v libx264 -crf 18 sdr.mp4
zscale=transfer=linear,tonemap=hable,zscale=transfer=bt709,format=yuv420p のように短くすると、色域(primaries)と色の計算式(matrix)が BT.2020 のまま残ります。
SDR 出力が色褪せて見える
トーンマッピングされていない(colorspace フィルタで変換した場合など)か、色域が BT.2020 のまま残っている(zscale=p=bt709 がない)と、色褪せて見えます。あわせて、入力の転送特性(transfer)が正しく検出されているかを確認します。tonemap の peak でピークの明るさを指定する場合は、最初の zscale の npl の明るさを 1 とした値で書きます。npl=100 なら、1000 nit の素材は peak=10 です(npl=50 なら peak=20)。peak=1000 と書くと暗くなります。
hable, mobius, reinhard、どの tonemap?
hable は暗い部分と明るい部分の細部を残しますが、全体がやや暗くなります。mobius は範囲内の色とコントラストをできるだけ保ちます。reinhard は全体の明るさを保ちますが、細部が平らになり、色の正確さも下がります。まず hable を試し、色の正確さを優先したいときは mobius に切り替えます。
HDR → SDR に GPU は要る?
要りません。zscale と tonemap は CPU で動きます。GPU で処理するフィルタには tonemap_opencl(OpenCL)と libplacebo(Vulkan)があり、gyan.dev の full ビルドなどに入っています。
tonemap でファイルサイズは変わる?
変わることはありますが、大きくなるか小さくなるかは映像によって違います。ファイルを小さくしたいときは、CRF の値を 1〜2 上げます。