toruのブログ

月1くらいで記事書きたい。

10bit 精度で 1bit のズレも発生しないテストパターン動画(H.265)を作成する

1. 背景

  • これまで幾つかのテストパターンを作ってきた
  • 動画の場合は ProRes や DNxHR などの業務用コーデックを使っていたがファイルサイズが大きくて困っていた(※1
  • H.265 の Lossless オプションを活用することでファイルサイズの削減と10bit精度での画質の維持(※2)が可能か調べることにした

※1 3840x2160, 60P を DNxHR の最高品質でエンコードすると 4秒で 1.62GB くらいになる
※2 「10bit精度での画質の維持」は 0~1023 の Code Value値が 1bitのズレも無く再現できることを意味する

2. おことわり

本記事は技術的に実現可能かどうかを調べただけの内容です。個人の趣味レベルの内容であり、業務レベルでの運用にはリスクが伴うと考えています。ネタ記事としてお楽しみください。

3. 目的

  • 10bit 精度で 1bit のズレも発生しない H.265 の動画を作成する条件を調べる
    • 今回の調査は Gray の Rampパターンが完全再現できることのみを確認する
    • Red, Green, Blue などの 色情報を持つデータについては確認しない
    • 動画の精度確認は動画ファイルを DaVinci Resolve で静止画シーケンスファイルにデコードして行う

4. 結論

  • テストパターン生成時に以下の4条件を満たすことで、10bit精度で 1bit のズレも生じない動画が作成可能
    • 動画ソースは 16bit 静止画シーケンスファイルとして用意する
    • 10bit のデータを 16bit に変換する際、10bit の 1023(0x3FF) が 16bit の 65324(0xFF2C) ~ 65352(0xFF48) となるように正規化する
    • ffmpeg で静止画シーケンスファイルをエンコードする際に lossless オプションを有効化する
    • ffmpeg で静止画シーケンスファイルをエンコードする際に bit深度のオプションを 12bit にする

5. 詳細

5.1. 最初の検証

5.1.1. 検証の準備

動画データは基本的に Limited Range で保存されるため、H.265 で 10bit精度の lossless エンコードしたとしても Full Range → Limited Range の変換が発生して bit欠損が生じてしまう。しかし 12bitエンコードすれば bit欠損が生じなくなると考えていた。

そこで筆者は以下に示す方法で 10bit精度の Rampパターンが 12bit の動画ファイルから再現可能か確認した。概要を図1に示す。

f:id:takuver4:20201205142831p:plain
図1. 検証環境の概要。FFmpeg と DaVinci は内部処理が全く分からないので ??? と書いてある

各ブロックの処理の詳細は以下の通り。

  • 処理①
    • Python で 10bit の Rampパターンのデータを生成(図2, 図3
      • 厳しいエンコード条件となるように背景は1フレームごとに値が変わるノイズデータとした
      • 最上段の 1 Line に 0~1023 の Rampパターンを入れた
    • 16bit に変換して png のシーケンスファイルとして保存
  • 処理②
    • FFmpeg で 12bit 精度で lossless エンコード
      • 作成した動画データは Google Drive 上 に置いた(クリックするとアクセスできます)
    • FFmpegffmpeg version 4.2.4-1ubuntu0.1 を使用
    • 使用したオプションは以下[1][2]
ffmpeg -color_primaries bt709 -color_trc bt709 -colorspace bt709 -r 24 -i input_seq_data_%4d.png -c:v libx265 -profile:v main444-12 -pix_fmt yuv444p12le -x265-params lossless=1 -color_primaries bt709 -color_trc bt709 -colorspace bt709 out.mp4
  • 処理③
    • DaVinci で 12bit の H.265 ファイルを 16bit TIFF で保存することで静止画シーケンスファイルを生成(※3
  • 処理④
    • 16bit の静止画シーケンスファイルを10bit精度に変換
    • 処理①で作成した Rampパターンと一致するかを確認
f:id:takuver4:20201206131749p:plain f:id:takuver4:20201206132248p:plain
図2. 作成した Rampパターンのスクリーンショット 図3. 図2の一部を拡大したもの

※3 本当は 10bit DPX で吐いて検証する予定だったのですが、なぜか DaVinci 17 Beta 3 の吐いた DPX を Python上で読むことができず 今回は TIFF で代用しました

5.1.2. 検証の結果

実際に 0~1023 CV の Ramp パターンを図1 の方法で再現した結果を表1に示す。予想とは異なり元の Ramp パターンを再現することはできなかった。

表1. Rampパターンを図1の④で再生成した結果
予想値 実際の値
0 0
1 1
2 2
3 3
252 253
253 254
254 255
255 256
508 510
509 510
510 512
511 512
764 766
765 767
766 768
767 769
1020 1023
1021 1023
1022 1023
1023 1023

表1 をよく見ると予想値の大きさに比例して誤差が大きくなっている。 この結果から筆者は 10bit Rampパターンを 16bit整数型に変換する際の係数のミスマッチによって誤差が生じていると推測した。

以後で、その推測に基づいて検証した内容を報告する。

5.2. 10bit to 16bit 変換

はじめに、整数型の 10bit のデータを 16bit に変換する方法についておさらいしておく。 筆者の知っている方法として次の2種類の方法がある。

  • (a) 16bitの最大値で正規化
    • 1023(0x3FF) → 65535(0xFFFF)
  • (b) bitシフト
    • 1023(0x3FF) → 65472(0xFFC0)

それぞれの動作イメージを図4、図5 に示す。

f:id:takuver4:20201205161421p:plain f:id:takuver4:20201206124039p:plain
図4. 16bitの最大値で正規化する例 図5. bitシフトで変換する例

(a), (b) と2種類書いてみたが実際のところ (a) を使うケースが殆どだと考えている。(b) は元のbit深度情報が分からないと正しい変換ができないため、使えるシーンが限定されるからである。 当然のことながら 5.1. の検証も (a) の方式で 10bit to 16bit 変換した。

しかし、表1 の結果を見ると値が大きくなっていることから、(a) の方式をそのまま使うのは NG であり 最大値を 65535(0xFFFF) よりも小さい値にする必要があると推測できる。

5.3. 適切な最大値の探索

表1の結果を踏まえて、筆者は正規化して 16bit変換する際の最大値(以後 "16bit の最大値"と略す)を 65280(0xFF00) や 65472(0xFFC0) に設定して再度実験を行った。しかし、残念ながら誤差は無くならなかった。

そこで探索によって誤差が発生しない 16bit の最大値を求めることにした。 以下の手順でテストデータを256種類作成し、それぞれの H.265 動画から 10bit の Ramp パターンが正確に復元できるかを調査した。

  • 16bit png の最大値を 0xFC00~0xFFFC まで 0x0004 刻みで変更
  • 16bit png のソースファイルから H.265 の YCbCr4:4:4,12bit の .mp4 ファイルを生成
  • DaVinci Resolve 17 Beta 3 でデコードし 16bit TIFF として出力
  • 16bit TIFF を 10bit に変換して(※4) 0~1023 の Code Value値が再現できているか確認

※4 この 16bit to 10bit 変換は 65535(0xFFFF) が 1023(0x3FF) となる変換式を使用しました。これまでの経験から DaVinci は 16bit のデータを扱う際に 0xFFFF が最大値となるように正規化することが分かっていたためです

5.4. 検証結果

16bit png の最大値を 0xFC00~0xFFFC まで 0x0004 刻みで変化させた際の理論値との誤差の累計をプロットした結果を図6 ~ 図7 に示す。 図より 10bit to 16bit 変換した際の最大値が 65324(0xFF2C) ~ 65352(0xFF48) の場合は誤差が0 となり 10bit の Rampパターンが完全に再現できることが分かる。

f:id:takuver4:20201206123836p:plain f:id:takuver4:20201206123847p:plain
図6. 誤差が0となる最大値の探索結果(全体) 図7. 誤差が0となる最大値の探索結果 (一部を拡大)

5.5. 考察

色々と気になる点が多く、業務での利用は困難だと思ってしまった。 気になった点をザッと列挙してみる。

  • 10bit の Rampパターンの再現には成功したが、最大値が 0xFF2C ~ 0xFF48 となるように正規化するのはマジックナンバー過ぎて気味が悪い
  • 図6, 図7 のデータをよく見ると 65320, 65360 付近で誤差が出たり出なかったりしていて不気味
  • 実は Dithering が適用されたりしてないか?(その辺の事前調査が全くできていない)
  • 図1 の ④ の 16bit to 10bit 変換は本当に正しいやり方となっているのか?
    • DaVinci の出力は 10bit DPX で行うべきだった(DaVinci 17 の DPX だけ手元の OpenImageIO だと開けないんですよ、何でですかね?)
  • そもそも FFmpeg について知らないことが多すぎて不安である

6. 感想

考察していると突っ込みどころがどんどん出てきてしまう内容であった。 もっと FFmpeg について詳しくなって いつか業務レベルで使えるようになりたい。

7. 付録

検証に使用した Pythonコードへのリンクを残しておく(カスタムした筆者の環境でしか動かないと思うが一応リンクを貼っておく)

7.1. 図1 の ①、②、④ の処理

github.com

  • ① は create_source_png_sequence
  • ② は encode_src_sequence
  • ④ は evaluate_reproduced_ramp

7.2. 図1 の ③ の処理

github.com

探索の際、DaVinci でデコードするファイルが多かったのでスクリプトで一括デコードした。 DaVinci で メニューバーの ”Workspace" → "Console" を選択し、出てきたウィンドウで ”Py3” を選択し、上記のソースを以下のようにして呼べば動く(事前に動画ファイルを指定されたフォルダに置いておく必要がある)。

import DV17_Decode as dv17
dv17.main_func2()

参考資料

[1] ニコラボ, "色の情報の扱いについて", https://nico-lab.net/setting_in_out_color_with_ffmpeg/

[2] FFmpeg, "H.265/HEVC Video Encoding Guide", https://trac.ffmpeg.org/wiki/Encode/H.265