toruのブログ

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

可逆圧縮する場合に Chroma subsampling に 4:2:0 を指定するのはキケン

1. 背景

  • 前回の記事 の応用として「iPhone 13 を使った CRTライクなインターレース表示」用の動画をエンコードしていた
  • インターレース用に挿入する黒ラインの太さは 2px に設定していたため「まあ、Main プロファイル (4:2:0) でも大丈夫やろ」と思っていたのだが、全然そんなことはなく、色の破綻が発生した
  • 原因究明に至るまでに Chroma subsampling に関して幾つかの知識を得られたので、それをメモとして残すことにした

2. おことわり

例によって未来の自分向けの記事です。前提条件など色々と飛ばして書いています。ご了承下さい。

3. 結論

  • 筆者が日常的に使用しているエンコーダーでは、4:2:0 を指定した場合は可逆圧縮でも想定通りの結果が得られなかった
    • 原因は 4:4:4 to 4:2:0 変換時の Cb、Cr 値が筆者の想定と異なっていたため
    • また残念ながら、4:4:4 to 4:2:0 変換時の Cb、Cr 値の算出方法は外部から制御することができなかった
  • テストパターン作成時に 4:2:0 を指定するのはキケンだからやめよう
    • テストパターン作成時は 4:4:4 を指定しよう

4. 結論に至るまでの経緯

4.1. そもそも何をしようとしていたのか

始めに、そもそも何をするつもりだったのか概要を書いておく。

4.1.1. CRTライクなインターレース表示

諸事情により筆者は図1~図4に示すようなインターレース表示を iPhone 13 Pro で試していた(※1)。 具体的には 60 fpsプログレッシブ信号に対して 2px または 4px の高さの黒ラインを挿入した動画を作成し、iPhone 13 Pro で表示していた。 筆者はこれを「CRTライクなインターレース表示」と呼んでいる。

※1 厳密に言うと黒フレーム挿入も行うのだが、本記事の本筋とは異なるためここでは特に触れない。

図1. フレーム番号 0 図2. フレーム番号 1 図3. フレーム番号 2 図1. フレーム番号 3

4.2. 発生した問題の詳細

前述の通り、黒ラインの高さは 2px or 4px としていた。そのため筆者は動画を H.265 の Mainプロファイル (Chroma subsampling 4:2:0) で可逆圧縮しても問題ないと考えていた。なぜならば、図5 に示すように 4:2:0 は 2x2 px の領域で Cb, Cr 成分を決定する方式だからである。黒ラインの高さが 2の倍数である分にはエンコード時の情報欠損が無いと考えていた(※2)。

※2 もう少し細かく書くと、Cb, Cr 成分は 2x2 px の加重平均で求まると考えていた。そのため、黒ラインの垂直方向の開始位置が偶数px であれば問題ないと考えていた。

図5. 4:2:0 における Cb, Cr 成分のマッピング(黄色枠の部分)。Wikipedia[1] より引用

しかし予想と異なり、実際は Main プロファイルで可逆圧縮すると予期せぬ色ズレが生じた。DaVinci Resolve で Main プロファイル (4:2:0) で圧縮した結果と Main 4:4:4 プロファイルで圧縮した結果の比較を図6に示す。

図6. 4:2:0 と 4:4:4 の違い

見て分かるとおり、Main プロファイルでは絵が暗くなっていることが分かる。

筆者は、どうしてこのような結果になったのか気になったため、追加調査を行うことにした。

4.3. 追加調査

適当なキーワードで Google 検索したところ、ITU-T H-series Recommendations - Supplement 19 [2] で気になる記述を見つけた。該当箇所を引用する(日本語訳は DeepL を使用した)。

For 4:2:0 chroma subsampling operations, it is important to make known the relative location alignment of the initial subsampling location processing of the content to avoid unnecessary quality degradation upon further content processing. For the purposes of this document, this property is described in terms of the ChromaLocType variable as defined in HEVC, which further corresponds with the value of the syntax elements chroma_sample_loc_type_top_field and chroma_sample_loc_type_bottom_field in HEVC and AVC. For NCG material, the usual alignment corresponds to ChromaLocType equal to 0 (vertically interstitial). For WCG material, the usual alignment corresponds to ChromaLocType equal to 2 (co-sited).


4:2:0 のクロマサブサンプリング処理では、コンテンツの最初のサブサンプリング位置処理の相対的な位置合わせを知ることが、その後のコンテンツ処理での不要な品質劣化を避けるために重要である。本書では、この特性を HEVC で定義されている ChromaLocType 変数で記述し、さらに HEVC および AVC のシンタックス要素 chroma_sample_loc_type_top_field および chroma_sample_loc_type_bottom_field の値で対応するものとする。NCG 素材の場合、通常のアライメントは ChromaLocType=0 (vertically interstitial) に相当する。WCG 素材の場合、通常のアライメントは ChromaLocType が 2(co-sited) に相当する。

筆者は ChromaLocType が何か知らなかったので H.265 の仕様書 [3] を参照した。すると以下の図7が見つかった。

図7. ChromaLocType の説明。H.265 の仕様書[3] より引用

これにより 4:2:0 において Cb, Cr は 2x2 px の加重平均とは限らないことが分かった。例えば ChromaLocType 2 では中心を含めた周囲の 9 px から Cb, Cr 値を決定している。この場合、冒頭で説明した CRTライクなインターレース表示の動画は可逆圧縮エンコードしたところで、確実に色ズレが発生してしまう。

以上の結果より冒頭の結論に書いた内容に至った。以上!

付録. FFmpeg と x265 を使った確認

以下の内容は本記事の主題からすると完全に蛇足なのだが、せっかく調査したのでメモを残しておく。

筆者は ChromaLocType = 1 を指定すれば、CRTライクなインターレース表示においては Main プロファイル (4:2:0) でも意図した可逆圧縮が可能になると考えた。 そこでパラメータの細かい制御ができない DaVinci Resolve ではなく FFmpeg と x265 を使って確認をしてみることにした。

x265 でエンコードを行うには、事前に 4:2:0 の YUV形式に変換が必要である。 YUV形式への変換は FFmpeg-c:v rawvideo -pix_fmt yuv420p オプションを指定すれば良い<要出典>。

今回は、図8 の画像を以下のオプションで変換した。

図8. FFmpeg を使った 4:4:4 to 4:2:0 変換の確認用パターン

ffmpeg -color_primaries bt709 -color_trc bt709 -colorspace bt709 -r 24 -loop 1 -i ./img/ana_420_offset_0-0.png -t 1 -c:v rawvideo -pix_fmt yuv420p -chroma_sample_location center ./videos/ana_420_offset_fmt-yuv420p_0-0.yuv

図8 は 16x12 のパターンである。dot mesh のサイズは 2x2、左端には 1px 幅の白ラインがある(バイナリエディタ確認用)。 理論値は以下である。

  • White: (Y, Cb, Cr) = (0xEB, 0x80, 0x80)
  • Magenta: (Y, Cb, Cr) = (0x4E, 0xD6, 0xE6)
  • Black: (Y, Cb, Cr) = (0x10, 0x80, 0x80)

なお理論値は以下で求めた。

    from colour import RGB_to_YCbCr, WEIGHTS_YCBCR
    K_709 = WEIGHTS_YCBCR['ITU-R BT.709']
    rgb = np.array([1, 1, 1])
    ycbcr = RGB_to_YCbCr(
        rgb, K=K_709, in_int=False, out_int=True,
        in_legal=False, out_legal=True, out_bits=8)
    print(f"0x{ycbcr[0]:02X}, 0x{ycbcr[1]:02X}, 0x{ycbcr[2]:02X}")

    rgb = np.array([1, 0, 1])
    ycbcr = RGB_to_YCbCr(
        rgb, K=K_709, in_int=False, out_int=True,
        in_legal=False, out_legal=True, out_bits=8)
    print(f"0x{ycbcr[0]:02X}, 0x{ycbcr[1]:02X}, 0x{ycbcr[2]:02X}")

    rgb = np.array([0, 0, 0])
    ycbcr = RGB_to_YCbCr(
        rgb, K=K_709, in_int=False, out_int=True,
        in_legal=False, out_legal=True, out_bits=8)
    print(f"0x{ycbcr[0]:02X}, 0x{ycbcr[1]:02X}, 0x{ycbcr[2]:02X}")

変換後の .yuv ファイルを VS Code の Hex Editor で確認したところ以下だった。

図9. .yuvファイルを Hex Editor で見た様子

そもそも H.265 でエンコードする前に 4:4:4 to 4:2:0 変換した時点で微妙にズレている。これではテストパターン生成時には使えない。残念無念。

ということで 4:2:0 を使うのは完全に諦めた。

感想

やっぱり検証用の動画は 4:4:4 でエンコードしないとダメですね…。

参考文献

[1] Wikipedia, "Chroma subsampling", https://en.wikipedia.org/wiki/Chroma_subsampling

[2] ITU-T H-series Recommendations - Supplement 19, "Usage of video signal type code points", https://www.itu.int/rec/T-REC-H.Sup19/en

[3] Recommendation ITU-T H.265, "High efficiency video coding", https://www.itu.int/rec/T-REC-H.265

[?] Douglas A Kerr, "Chrominance Subsampling in Digital Images", "http://dougkerr.net/Pumpkin/articles/Subsampling.pdf"