32blogby Studio Mitsu

FFmpeg HDR→SDR変換:トーンマッピング完全ガイド

FFmpegでHDR動画をSDRに変換する方法。zscale+tonemapとlibplaceboの2つのパイプライン、hable・reinhard等のアルゴリズム比較、GPU高速化、色褪せ対策まで解説。

by omitsu23 min read
FFmpegCLIHDRtonemappingvideo-encodingvideo-processingcolor spaceGPUcommands
目次

FFmpegでHDR動画をSDRに変換するには トーンマッピング が必要だ。HDRの広い輝度・色域(BT.2020/PQ)をSDRの範囲(BT.709)に圧縮する処理で、単なる再エンコードでは色が破綻する。すぐ使えるコマンドはこれ:

bash
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+tonemap CPUパイプラインの仕組み
  • libplacebo GPUパイプラインの使い方
  • アルゴリズムの選び方(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)またはHLGBT.709ガンマ(約2.4)
色域BT.2020(広色域)BT.709(標準色域)
ビット深度10bit8bit
ピーク輝度1,000〜10,000ニト約100ニト

コンテナやコーデックを変えるだけでは、これらの変換は行われない。トーンマッピング で広い輝度範囲をSDRに数学的にマッピングし、色空間変換 でBT.2020からBT.709に変換する — この2ステップが必要だ。

ffprobeで入力を確認する

変換前に、ソースが本当にHDRかどうか確認しよう:

bash
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 → HLG
  • bt2020nc → 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不要。

フィルタチェーン全体

bash
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パイプラインより目に見えて良い結果を出す。

基本コマンド

bash
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 + tonemaplibplacebo
ピーク検出静的(メタデータ依存)動的(フレーム単位のヒストグラム解析)
ガマットマッピング基本的な脱色のみ6種類以上(perceptual、relative、saturation等)
ディザリングなし(formatフィルタ任せ)内蔵(blue noise、ordered)
アルゴリズム7種類12種類(BT.2390、ST 2094-40含む)
Dolby Vision非対応Profile 5/8.x対応
シーンチェンジ検出なし閾値設定可能
コントラスト回復なし内蔵(デフォルト0.30)
処理CPUGPU(Vulkan)

最大の実用的な違いは 動的ピーク検出 だ。静的MaxCLLメタデータ(不正確だったり欠落していることが多い)に頼る代わりに、libplaceboはフレームごとに実際の輝度ヒストグラムを解析してトーンマッピングカーブをリアルタイムに調整する。暗いシーンが不必要に暗くなったり、明るいシーンが飛んだりすることを防げる。

libplacebo + NVIDIAハードウェアデコード

bash
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ワークフローにするなら libx264h264_nvenc -cq 22 に置き換える。

トーンマッピングアルゴリズムの比較

FFmpeg組み込みの tonemap フィルタは7つのアルゴリズムを提供している:

アルゴリズム特徴向いている場面
hableフィルミックS字カーブ。暗部・ハイライト両方のディテールを保持汎用。コミュニティの定番
reinhardグローバル輝度を保持。やや明るい出力明るさがコントラストより重要なコンテンツ
mobius範囲内の色精度を保ちつつ、範囲外を滑らかにロールオフ色精度が重要な作業
clip境界でハードクリップ。範囲内の色精度が最高ダイナミックレンジが低いHDR(ピーク400ニト未満)
linear全範囲をリニアスケール特殊用途。通常の視聴には不向き
gammaカーブ間の対数転送ニッチな用途
noneトーンマップなし、範囲外の脱色のみテスト/デバッグ

ほとんどのHDR→SDR変換は hable + desat=0 で始めよう。この組み合わせはStack OverflowDoom9フォーラムでほぼ定石として語られている。結果が暗すぎる場合は 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メタデータを使う:

bash
# 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と後方互換性がある。それでもトーンマッピングした方が良い結果になる:

bash
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 なら活用できる:

bash
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 に対応:

bash
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)

bash
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)

bash
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)

bash
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 slow18〜22互換性最優先。ほぼ全デバイスで再生可能
libx265 -crf 22 -preset medium20〜24H.264比で約40%のサイズ削減
libsvtav1 -crf 32 -preset 428〜36最高の圧縮効率。対応デバイスは増加中
h264_nvenc -cq 22 -preset p420〜26GPUハードウェアエンコード。高速だがファイルは大きめ

アーカイブなら libx265libsvtav1 、共有や確認用なら libx264 が安全だ。

bash
# 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ファイルを一括変換:

bash
#!/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つ:

  1. トーンマッピングなし — tonemapフィルタを通さずに再エンコードしている
  2. 脱色が強すぎる — デフォルトの desat=2.0 は積極的に色を抜く。desat=0 に設定
  3. フィルタ順序の誤り — トーンマッピングの前にリニアライズする必要がある

対処: 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メタデータを検出し、変換済みの映像に自分のトーンマッピングを重ねて適用してしまう。二重処理のアーティファクトが出る。

対処: トーンマッピング後にサイドデータを削除:

bash
# フィルタチェーンの末尾(format=yuv420pの前)に追加
...,sidedata=delete

変換後もffprobeがBT.2020を表示する

出力ファイルの色メタデータが正しく設定されていない場合がある。明示的なタグ付けを追加:

bash
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.2020BT.709どの実世界の色を表現できるか
マトリクス(YCbCr係数)bt2020ncbt709RGBを輝度+色差チャンネルにどう変換するか

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=trueDolby 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=yuv420pformat=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を日常的に使っているなら、以下の記事も参考になるはず: