FFmpegでHDR動画をSDRに変換するには トーンマッピング が必要だ。HDRの広い輝度・色域(BT.2020/PQ)をSDRの範囲(BT.709)に圧縮する処理で、単なる再エンコードでは色が破綻する。すぐ使えるコマンドはこれ:
ffmpeg -i input_hdr.mp4 \
-vf "zscale=t=linear:npl=100,format=gbrpf32le,zscale=p=bt709,tonemap=hable:desat=0,zscale=t=bt709:m=bt709:r=tv,format=yuv420p" \
-c:v libx264 -crf 18 -preset slow -c:a copy output_sdr.mp4
このガイドでは、CPUベースの zscale+tonemap とGPU加速の libplacebo の2つのパイプラインを解説する。用途に合った方を選んでほしい。
この記事でわかること
- HDR→SDR変換にトーンマッピングが必要な理由
zscale+tonemapCPUパイプラインの仕組みlibplaceboGPUパイプラインの使い方- アルゴリズムの選び方(hable, reinhard, mobius, bt.2390)
- HDR10、HLG、HDR10+、Dolby Visionの扱い方
- VAAPI、OpenCL、NVENCによるGPU高速化
- 色褪せ・バンディング・メタデータ問題の対処法
HDR動画を単純に再エンコードしてはいけない理由
HDR動画を変換しようとして、出力が色褪せたVHSみたいになった経験はないだろうか? ffmpeg -i hdr.mp4 -c:v libx264 output.mp4 と実行すると、結果は色褪せて暗部は潰れ、ハイライトは飛ぶ。RedditのFFmpegコミュニティやDoom9フォーラムでも定番の質問で、答えはいつも同じ — トーンマッピングが必要だ。原因を理解しておこう。
HDR動画はSDRディスプレイが正しく解釈できない3つの要素を持っている:
| プロパティ | HDR(典型例) | SDR |
|---|---|---|
| 伝達特性 | PQ(SMPTE ST 2084)またはHLG | BT.709ガンマ(約2.4) |
| 色域 | BT.2020(広色域) | BT.709(標準色域) |
| ビット深度 | 10bit | 8bit |
| ピーク輝度 | 1,000〜10,000ニト | 約100ニト |
コンテナやコーデックを変えるだけでは、これらの変換は行われない。トーンマッピング で広い輝度範囲をSDRに数学的にマッピングし、色空間変換 でBT.2020からBT.709に変換する — この2ステップが必要だ。
ffprobeで入力を確認する
変換前に、ソースが本当にHDRかどうか確認しよう:
ffprobe -v quiet -show_streams -select_streams v:0 input_hdr.mp4 2>&1 | grep -E "color_|pix_fmt"
以下のような出力が出ればHDR:
pix_fmt=yuv420p10le
color_space=bt2020nc
color_transfer=smpte2084
color_primaries=bt2020
smpte2084→ HDR10/HDR10+(PQ伝達特性)arib-std-b67→ HLGbt2020nc→ BT.2020非定輝度マトリクスyuv420p10le→ 10bit 4:2:0
全てのcolorフィールドが bt709 なら、その動画はすでにSDRだ。変換不要。
HDR10の静的メタデータ(MaxCLL/MaxFALL)を確認するには ffprobe -v quiet -show_frames -read_intervals "%+#1" input.mp4 | grep -E "mastering|content_light" を実行する。このメタデータがあると、トーンマッピングアルゴリズムの判断精度が上がる。
パイプライン1: zscale + tonemap(CPU)
最も互換性が高いアプローチだ。zscale(zimgベース)とtonemapフィルタがあれば動作する。GPU不要。
フィルタチェーン全体
ffmpeg -i input_hdr.mp4 \
-vf "zscale=t=linear:npl=100,format=gbrpf32le,zscale=p=bt709,tonemap=hable:desat=0,zscale=t=bt709:m=bt709:r=tv,format=yuv420p" \
-c:v libx264 -crf 18 -preset slow \
-c:a copy -movflags +faststart \
output_sdr.mp4
各フィルタの役割を順に見ていく。
ステップ1: 伝達特性をリニアライズ
zscale=t=linear:npl=100
PQ(知覚量子化)カーブを リニアライト に変換する。npl=100 はSDR基準の100ニトをピークに設定。これがトーンマッピングカーブのアンカーポイントになり、100ニトを超える輝度は圧縮される。
ステップ2: 浮動小数点RGBに変換
format=gbrpf32le
32bit浮動小数点プレーナRGB に切り替え。この中間フォーマットがないと、色計算の精度が落ちてバンディング(色の段差)が発生する。tonemapフィルタはRGB入力が必要で、浮動小数点なら整数丸めによるアーティファクトも回避できる。
ステップ3: 色域変換
zscale=p=bt709
BT.2020広色域 から BT.709標準色域 に色をマッピングする。BT.709の範囲外に出る超飽和の緑や赤はクリップされる。
ステップ4: トーンマッピング
tonemap=hable:desat=0
Hableフィルミックカーブで輝度範囲を圧縮する。desat=0 はハイライトの脱色を無効化 — これを付けないと、明るい部分の色が抜けてグレーっぽくなる。
ステップ5: 出力の色特性を設定
zscale=t=bt709:m=bt709:r=tv
BT.709ガンマカーブ を適用(t=bt709)、YCbCrマトリクス をBT.709に設定(m=bt709)、TVレンジ(16–235)に制約する。
ステップ6: 8bit YUVに変換
format=yuv420p
YUV 4:2:0 8bit に最終変換。SDRの標準ピクセルフォーマットで、プレイヤーの互換性が最も高い。
パイプライン2: libplacebo(Vulkan GPU加速)
libplaceboはmpvのレンダリングエンジンだ。トーンマッピング、ガマットマッピング、ディザリング、色管理を1つのGPU加速フィルタで処理でき、多くのコンテンツでCPUパイプラインより目に見えて良い結果を出す。
基本コマンド
ffmpeg -init_hw_device vulkan \
-i input_hdr.mp4 \
-vf "libplacebo=tonemapping=hable:peak_detect=true:gamut_mode=perceptual:colorspace=bt709:color_trc=bt709:color_primaries=bt709:range=limited:dithering=blue:format=yuv420p" \
-c:v libx264 -crf 18 -preset slow \
-c:a copy output_sdr.mp4
libplaceboが優れている理由
| 機能 | zscale + tonemap | libplacebo |
|---|---|---|
| ピーク検出 | 静的(メタデータ依存) | 動的(フレーム単位のヒストグラム解析) |
| ガマットマッピング | 基本的な脱色のみ | 6種類以上(perceptual、relative、saturation等) |
| ディザリング | なし(formatフィルタ任せ) | 内蔵(blue noise、ordered) |
| アルゴリズム | 7種類 | 12種類(BT.2390、ST 2094-40含む) |
| Dolby Vision | 非対応 | Profile 5/8.x対応 |
| シーンチェンジ検出 | なし | 閾値設定可能 |
| コントラスト回復 | なし | 内蔵(デフォルト0.30) |
| 処理 | CPU | GPU(Vulkan) |
最大の実用的な違いは 動的ピーク検出 だ。静的MaxCLLメタデータ(不正確だったり欠落していることが多い)に頼る代わりに、libplaceboはフレームごとに実際の輝度ヒストグラムを解析してトーンマッピングカーブをリアルタイムに調整する。暗いシーンが不必要に暗くなったり、明るいシーンが飛んだりすることを防げる。
libplacebo + NVIDIAハードウェアデコード
ffmpeg -init_hw_device vulkan=vk,disable_multiplane=1 \
-filter_hw_device vk \
-hwaccel cuda -hwaccel_output_format cuda \
-i input_hdr.mp4 \
-vf "hwupload=derive_device=vulkan,libplacebo=tonemapping=hable:peak_detect=true:colorspace=bt709:color_primaries=bt709:color_trc=bt709:gamut_mode=perceptual:format=yuv420p,hwdownload,format=yuv420p" \
-c:v libx264 -crf 18 -preset slow \
-c:a copy output_sdr.mp4
NVIDIA GPU(CUDA)でデコードし、Vulkanでトーンマッピング、その後CPUエンコード。完全なGPUワークフローにするなら libx264 を h264_nvenc -cq 22 に置き換える。
トーンマッピングアルゴリズムの比較
FFmpeg組み込みの tonemap フィルタは7つのアルゴリズムを提供している:
| アルゴリズム | 特徴 | 向いている場面 |
|---|---|---|
| hable | フィルミックS字カーブ。暗部・ハイライト両方のディテールを保持 | 汎用。コミュニティの定番 |
| reinhard | グローバル輝度を保持。やや明るい出力 | 明るさがコントラストより重要なコンテンツ |
| mobius | 範囲内の色精度を保ちつつ、範囲外を滑らかにロールオフ | 色精度が重要な作業 |
| clip | 境界でハードクリップ。範囲内の色精度が最高 | ダイナミックレンジが低いHDR(ピーク400ニト未満) |
| linear | 全範囲をリニアスケール | 特殊用途。通常の視聴には不向き |
| gamma | カーブ間の対数転送 | ニッチな用途 |
| none | トーンマップなし、範囲外の脱色のみ | テスト/デバッグ |
ほとんどのHDR→SDR変換は hable + desat=0 で始めよう。この組み合わせはStack OverflowやDoom9フォーラムでほぼ定石として語られている。結果が暗すぎる場合は reinhard を試す — コントラストは落ちるが明るい仕上がりになる。Jellyfinなどのメディアサーバーがデフォルトでreinhardを採用しているのもこの理由だ。
libplacebo専用アルゴリズム
libplaceboは組み込みフィルタにはないアルゴリズムも提供する:
- bt.2390 — ITU-R BT.2390 EETF。放送業界のHDR→SDR変換標準。エルミートスプラインロールオフ
- bt.2446a — ITU-R BT.2446 Method A。マスタリング済みHDRソースのクリエイティブ意図を保持
- st2094-40 — SMPTE ST 2094-40の動的メタデータ(HDR10+)を使ったシーン単位のトーンマッピング
- auto — libplaceboのデフォルト。入力メタデータから最適なアルゴリズムを自動選択
HDRフォーマット別の対応
HDR10(静的メタデータ)
最も一般的なフォーマット。PQ伝達特性と静的MaxCLL/MaxFALLメタデータを使う:
# zscale+tonemap — すべてのHDR10コンテンツで動作
ffmpeg -i hdr10_input.mkv \
-vf "zscale=t=linear:npl=100,format=gbrpf32le,zscale=p=bt709,tonemap=hable:desat=0,zscale=t=bt709:m=bt709:r=tv,format=yuv420p" \
-c:v libx264 -crf 18 -c:a copy output_sdr.mp4
HLG(Hybrid Log-Gamma)
HLGは設計上SDRと後方互換性がある。それでもトーンマッピングした方が良い結果になる:
ffmpeg -i hlg_input.mkv \
-vf "zscale=tin=arib-std-b67:t=linear:npl=100,format=gbrpf32le,zscale=p=bt709,tonemap=hable:desat=0,zscale=t=bt709:m=bt709:r=tv,format=yuv420p" \
-c:v libx264 -crf 18 -c:a copy output_sdr.mp4
tin=arib-std-b67 で入力がHLG伝達特性であることを明示する点に注意。
HDR10+(動的メタデータ)
HDR10+はHDR10の上にシーン単位の輝度メタデータを追加する。組み込みtonemapフィルタはこれを無視するが、libplacebo なら活用できる:
ffmpeg -init_hw_device vulkan \
-i hdr10plus_input.mkv \
-vf "libplacebo=tonemapping=st2094-40:peak_detect=true:colorspace=bt709:color_primaries=bt709:color_trc=bt709:format=yuv420p" \
-c:v libx264 -crf 18 -c:a copy output_sdr.mp4
st2094-40 アルゴリズムが動的メタデータを読み取り、シーンごとにトーンマッピングを調整する。暗いシーンは暗いまま、明るいシーンはハイライトを適切に圧縮する。
Dolby Vision
FFmpegのDolby Vision対応は限定的だが改善が進んでいる。libplaceboは Profile 5と8.x に対応:
ffmpeg -init_hw_device vulkan \
-i dolby_vision_input.mkv \
-vf "libplacebo=tonemapping=hable:apply_dolbyvision=true:peak_detect=true:colorspace=bt709:color_primaries=bt709:color_trc=bt709:format=yuv420p" \
-c:v libx264 -crf 18 -c:a copy output_sdr.mp4
GPU高速化オプション
CPUのtonemapパイプラインは4Kコンテンツで約10fps。もっと速くしたいなら、GPU加速の選択肢がある。
OpenCL(AMD/NVIDIA/Intel)
ffmpeg -init_hw_device opencl=ocl \
-filter_hw_device ocl \
-i input_hdr.mp4 \
-vf "format=p010,hwupload,tonemap_opencl=tonemap=hable:desat=0:t=bt709:m=bt709:p=bt709:format=nv12,hwdownload,format=nv12" \
-c:v libx264 -crf 18 -c:a copy output_sdr.mp4
tonemap_openclはほとんどのGPUで動くが、P010(10bit)入力フォーマットが必要。
VAAPI(Intel/AMD)
ffmpeg -hwaccel vaapi -hwaccel_output_format vaapi \
-i input_hdr.mp4 \
-vf "tonemap_vaapi=format=nv12:t=bt709:m=bt709:p=bt709" \
-c:v h264_vaapi -qp 18 -c:a copy output_sdr.mp4
tonemap_vaapiはデコード、トーンマップ、エンコードの全てをGPU上で完結させる。
フルNVIDIAパイプライン(NVDEC → Vulkan → NVENC)
ffmpeg -init_hw_device vulkan=vk,disable_multiplane=1 \
-filter_hw_device vk \
-hwaccel cuda -hwaccel_output_format cuda \
-i input_hdr.mp4 \
-vf "hwupload=derive_device=vulkan,libplacebo=tonemapping=hable:peak_detect=true:colorspace=bt709:color_primaries=bt709:color_trc=bt709:format=yuv420p,hwupload=derive_device=cuda" \
-c:v h264_nvenc -cq 22 -preset p4 \
-c:a copy output_sdr.mp4
NVIDIAハードウェアでの最速オプション。ハードウェアデコード(NVDEC)、GPUトーンマッピング(Vulkan/libplacebo)、ハードウェアエンコード(NVENC)。最新GPUなら4Kで60fps以上が出る。
パフォーマンス比較(目安、4K HEVC HDR10 → H.264 SDR)
| パイプライン | 速度 | 品質 |
|---|---|---|
| zscale + tonemap(CPU) | 約10fps | 良好 |
| tonemap_opencl(GPU) | 約40fps | 良好 |
| tonemap_vaapi(Intel iGPU) | 約30fps | 許容範囲 |
| libplacebo Vulkan(GPU) | 約25fps | 最高 |
| NVDEC → libplacebo → NVENC | 約60fps | 最高 |
SDR出力のエンコーダ選び
トーンマッピング後、SDR結果をエンコードする。用途別のガイド:
| エンコーダ | CRF/CQ目安 | 用途 |
|---|---|---|
libx264 -crf 18 -preset slow | 18〜22 | 互換性最優先。ほぼ全デバイスで再生可能 |
libx265 -crf 22 -preset medium | 20〜24 | H.264比で約40%のサイズ削減 |
libsvtav1 -crf 32 -preset 4 | 28〜36 | 最高の圧縮効率。対応デバイスは増加中 |
h264_nvenc -cq 22 -preset p4 | 20〜26 | GPUハードウェアエンコード。高速だがファイルは大きめ |
アーカイブなら libx265 か libsvtav1 、共有や確認用なら libx264 が安全だ。
# SVT-AV1の例 — サイズ効率が良い
ffmpeg -i input_hdr.mp4 \
-vf "zscale=t=linear:npl=100,format=gbrpf32le,zscale=p=bt709,tonemap=hable:desat=0,zscale=t=bt709:m=bt709:r=tv,format=yuv420p" \
-c:v libsvtav1 -crf 32 -preset 4 \
-svtav1-params tune=0 \
-c:a libopus -b:a 128k \
output_sdr.mkv
バッチ処理
シェルループで複数のHDRファイルを一括変換:
#!/bin/bash
# batch-hdr-to-sdr.sh — カレントディレクトリの全.mkvファイルをHDR→SDR変換
for f in *.mkv; do
echo "変換中: $f"
ffmpeg -y -i "$f" \
-vf "zscale=t=linear:npl=100,format=gbrpf32le,zscale=p=bt709,tonemap=hable:desat=0,zscale=t=bt709:m=bt709:r=tv,format=yuv420p" \
-c:v libx264 -crf 18 -preset medium \
-c:a copy \
-movflags +faststart \
"${f%.mkv}_sdr.mp4"
done
echo "完了。$(ls -1 *_sdr.mp4 2>/dev/null | wc -l) ファイルを変換しました。"
進捗表示付きのPythonバッチ処理については FFmpeg Python自動化ガイド を参照。
よくある問題の対処法
「ffmpeg hdr sdr 色褪せ」で検索してここに来た人も多いだろう。HDR変換で最も聞かれる問題をまとめた��
色褪せ(ウォッシュアウト)
HDR変換を初めてやる人がほぼ全員ぶつかる壁。原因は大体以下の3つ:
- トーンマッピングなし — tonemapフィルタを通さずに再エンコードしている
- 脱色が強すぎる — デフォルトの
desat=2.0は積極的に色を抜く。desat=0に設定 - フィルタ順序の誤り — トーンマッピングの前にリニアライズする必要がある
対処: zscale+tonemap のフルチェーンを desat=0 で使うか、libplaceboに切り替える(自動処理される)。
カラーバンディング(ポスタリゼーション)
空のグラデーションなどに段差が見える。10bit→8bitの量子化が原因。
対処法:
- libplaceboの
dithering=blueを使う(最善) - 出力を10bitに保持:
format=yuv420p10le+libx265 -crf 22(H.265/AV1は10bitがデフォルト) - フィルムグレインでバンディングをマスク:
libsvtav1 -svtav1-params film-grain=8
出力が暗すぎる
Hableのフィルミックカーブはハイライトを積極的に圧縮するため、コンテンツによっては想定より暗くなる。
対処:
tonemap=reinhard:desat=0を試す — 明るい出力になるnpl(ノミナルピーク輝度)を調整: 値が大きいほど明るくなる。npl=200を試す- libplaceboなら
contrast_recovery=0.5でミッドトーンのコントラストを回復できる
HDRメタデータが残る
一部プレイヤーが残留HDRメタデータを検出し、変換済みの映像に自分のトーンマッピングを重ねて適用してしまう。二重処理のアーティファクトが出る。
対処: トーンマッピング後にサイドデータを削除:
# フィルタチェーンの末尾(format=yuv420pの前)に追加
...,sidedata=delete
変換後もffprobeがBT.2020を表示する
出力ファイルの色メタデータが正しく設定されていない場合がある。明示的なタグ付けを追加:
ffmpeg -i input_hdr.mp4 \
-vf "zscale=t=linear:npl=100,format=gbrpf32le,zscale=p=bt709,tonemap=hable:desat=0,zscale=t=bt709:m=bt709:r=tv,format=yuv420p" \
-c:v libx264 -crf 18 \
-colorspace bt709 -color_primaries bt709 -color_trc bt709 \
-c:a copy output_sdr.mp4
-colorspace、-color_primaries、-color_trc フラグで出力ストリームに正しいメタデータを設定する。
色科学の基礎
フィルタチェーンがなぜあの順序で動くのか、背景にある色科学を理解しておこう。
色変換の3つの軸
| 軸 | HDR値 | SDR値 | 制御するもの |
|---|---|---|---|
| 伝達特性(EOTF/OETF) | PQ(ST 2084)またはHLG(ARIB STD-B67) | BT.709ガンマ | 輝度を信号値にどうエンコードするか |
| 色域(Primaries) | BT.2020 | BT.709 | どの実世界の色を表現できるか |
| マトリクス(YCbCr係数) | bt2020nc | bt709 | RGBを輝度+色差チャンネルにどう変換するか |
zscale+tonemap パイプラインでは各軸を独立して変換する。libplaceboは内部で3軸を一度に処理する。
なぜ最初にリニアライズするのか
PQ伝達特性は 知覚的に均一 — 信号値の等間隔ステップが知覚輝度の等間隔ステップに対応する。しかしトーンマッピングの数学は リニアライト で動く。値を2倍にすると物理的な光強度も2倍になる空間だ。PQ空間でトーンマッピングすると、暗部と明部が不均一に歪む。
なぜ浮動小数点なのか
10bit整数は1,024レベル。リニアライトに変換すると値の分布が極端に偏り、ほとんどの値がゼロ付近に集中する。浮動小数点なら暗部のバンディングの原因となる精度低下を回避できる。
FAQ
HDR10とHDR10+の違いは?
HDR10は 静的メタデータ を使う — 動画全体に対して1つの輝度値(MaxCLL/MaxFALL)を持つ。HDR10+は 動的メタデータ を追加し、シーンごとに輝度情報を持つため、暗い映画シーンと明るい屋外シーンそれぞれで最適なトーンマッピングが可能になる。FFmpeg組み込みのtonemapは動的メタデータを無視するが、libplaceboの st2094-40 アルゴリズムなら活用できる。
どのトーンマッピングアルゴリズムを使うべき?
まず hable(フィルミックカーブ)+ desat=0 で始めよう。暗部とハイライト両方のディテールを保持する。結果が暗すぎるなら reinhard で明るい出力に。放送業務なら bt.2390(ITU標準)をlibplaceboで。HDR10+コンテンツには動的メタデータを活用する st2094-40 がベスト。
Dolby Visionコンテンツのトーンマッピングは可能?
部分的に可能。libplaceboは apply_dolbyvision=true で Dolby Vision Profile 5と8.x に対応。Profile 7(デュアルレイヤー)は完全対応していないため、dovi_toolでベースレイヤーを先に抽出する必要がある。組み込みの zscale+tonemap パイプラインはDolby Visionに非対応。
変換後に色褪せて見えるのはなぜ?
よくある原因は3つ:(1)tonemapフィルタを適用せずに再エンコードしている。(2)desat パラメータが高すぎる — 0 に設定する。(3)フィルタ順序が間違っている — トーンマッピングの前に zscale=t=linear でリニアライズが必要。詳細は よくある問題の対処法 を参照。
libplaceboはzscale+tonemapより良い?
品質面では、イエス。libplaceboの動的ピーク検出、内蔵ディザリング、高度なガマットマッピングは、多くのケースで視覚的に優れた結果を出す。トレードオフはVulkan GPU対応と --enable-libplacebo 付きのカスタムFFmpegビルドが必要な点。GPU無しのサーバーでの簡易変換なら zscale+tonemap で十分。
バンディングを避けるため10bit出力にするには?
フィルタチェーンの format=yuv420p を format=yuv420p10le に置き換え、10bit対応エンコーダ(libx265またはlibsvtav1 — どちらも10bitがデフォルト)を使う。H.264は多くの実装で8bit専用。
GPU vs CPUでどれくらい速度差がある?
4K HEVC HDR10ソースの場合: CPU zscale+tonemap で約10fps、OpenCLで約40fps、NVDEC → libplacebo → NVENCで60fps以上。実際の速度はGPU、エンコーダ設定、入力の複雑さによる。パイプライン別の比較は GPU高速化オプション を参照。
トーンマッピングで品質は劣化する?
する。広い色域・輝度空間から狭い空間への変換は本質的にロスがある — 1,000ニトの輝度範囲を100ニトに収めるには圧縮が必須だ。トーンマッピングの目的は知覚的な品質劣化を最小限にすること。hableまたはbt.2390に desat=0、10bit出力、ディザリングの組み合わせが最善の結果を出す。
まとめ
手早くHDR→SDR変換するなら、zscale+tonemap=hable:desat=0 パイプラインが大半のコンテンツをきれいに処理する。品質にこだわるケースやHDR10+/Dolby Visionを扱う場合は、libplaceboのセットアップに投資する価値がある。
押さえておくべきポイント:
- 必ずトーンマッピングする — HDR動画を色空間変換なしに再エンコードしない
desat=0を使う — デフォルトの脱色はハイライトの色を潰すformat=gbrpf32leを使う — 浮動小数点中間フォーマットがバンディングを防ぐ- ffprobeで確認する — 出力が実際にBT.709の色特性を報告しているか検証する
FFmpegを日常的に使っているなら、以下の記事も参考になるはず:
- FFmpegの使い方チュートリアル — コマンドの基本操作
- FFmpeg GPU高速化ガイド — NVENCとQSVハードウェアアクセラレーション
- SVT-AV1最適設定ガイド — AV1出力のCRFとプリセット
- FFmpeg動画圧縮ガイド — コーデック比較と品質調整