0. おことわり
この記事は筆者の備忘録として書いており、一般の方が読むことを想定していません。
未来の自分が読み返して理解できるように、できるだけ分かりやすく書くつもりですが、前提となる説明が省略されている箇所が多数あります。ご了承下さい。
1. 背景
- 昨年に BT.2407 実装の効果確認のために 図1に示す Hue-Chromaテストパターンを作成した
- 最近、とある作業で当該パターンを使ったところ Yellow で想定外の色ズレを発見した
- 個人的に許せない事だったので修正することにした
2. 目的
- 昨年に作成した Hue-Chromaテストパターンの色ズレの原因を特定する
- 色ズレを修正したテストパターンを作成する
3. 結論
- 色ズレ の原因は CIELAB 空間での Yellow の BT.709 Cups と BT.2020 Cups の位置関係の考察不足
- 設定した Focal Point に問題があり BT.709 の Gamut内を想定したパッチが BT.709 の Gamut外の値になっていた
- 詳細は 4.3. を参照
- 修正前の色ズレのあるテストパターンを図1に、修正後のテストパターンを図2に示す。
- テストパターンの各ブロックの左上に "■" or "□" マークのある箇所は BT.709 の色域外を意味している
- 修正前のパターンでは Yellow において想定外の箇所に "■" マークが出てしまっている
図1. 修正前(色ズレあり) | 図2. 修正後(色ズレなし) |
4. 詳細
4.1. テストパターンの特性
まずは作成したテストパターンの想定する特性について説明する。 下の図3 に書いた通り、このテストパターンは CIELAB色空間で 水平方向で Hue を、垂直方向で Chroma を変化させたものである。 加えて、各パッチが BT.709 の色域内か判断できるように BT.709 の Gamut Boundary を超えた場合はパッチの左上に小さな ”■” マークを付与してある。
4.2. テストパターンの作成方法
続いてテストパターンの具体的な作成方法について説明する。
まず概要から説明する。このパターンは以下の2つを満たすように作成した。
- ① 垂直方向でなるべく BT.709 の領域が広くなる
- ② 垂直方向の再下段は各Hue で最も Chroma が大きな値(BT.2020 Cups)となる
Chroma-Lightness 平面での例を以下の図4 に示す。条件①と②を満たすように 各Hue のパッチは BT.2020 Cups と BT.709 Cups を結んだ直線上の値を取るようにした。 図4 は Hue=40° の例であるが、これを各Hue において実施することで最終的なパターンを作成した。
続いて、BT.709 Cups、BT.2020 Cups の求め方について説明する。まず初めに BT.709 と BT.2020 の Gamut Boundary を求めた。 今回は以下の手順で求めた。
- 任意の Hue の Chroma-Lightness 平面を Chroma 方向に N_c個、Lightness方向に N_l個に分割する(N_c=20, N_l=10 の例を図5に示す)
- 各ブロックの LCH値を RGB値に変換し、0 ≦ R ≦ 1 かつ 0 ≦ G ≦ 1 かつ 0 ≦ B ≦ 1 を満たすブロックを有効ブロックとする(有効ブロックを黄色にした例を図6に示す)
- 各 Lightness値に対して Chromaが大きくなる方向にブロックを走査し、最初に無効ブロックとなる直前のブロックを、その Lightness値の Gamut Boundary とする(Gamut Boundary を黒色にした例を図7に示す)
- これを Hue を少しずつ変更しながら 0~360°の範囲で行う
図5. Chroma-Lightness 平面をブロックに分割した例 | 図6. 有効ブロック (Out-of-Gamut でない領域)を黄色で塗った様子 | 図7. Gamut Boundary を黒色で塗った様子 |
もう少しブロック分割を細かくして、N_c=240, N_l=135 とした例を 図8, 図9 に示す。
図8. N_c=240, N_l=135, Hue=40° の例 | 図9. N_c=240, N_l=135, Hue=99° の例 |
更にブロック分割を細かくして、N_c=1024, N_l=32768 とした例を 図10, 図11 に示す。
図10. N_c=1024, N_l=32768, Hue=40° の例 | 図11. N_c=1024, N_l=32768, Hue=99° の例 |
なお、上記で計算した Gamut Boundary は LUT化して気楽に使えるようにした。自分が使い方を思い出せるようにサンプルコードを以下に示す( なお、本コードは筆者環境以外では動かないことが予想される)。
# LUT の作成 from create_gamut_booundary_lut import calc_chroma_boundary_lut import color_space as cs import numpy as np if __name__ == '__main__': hue_sample = 1024 chroma_sample = 32768 ll_num = 1024 cs_name = cs.BT2020 lut = calc_chroma_boundary_lut( lightness_sample=ll_num, chroma_sample=chroma_sample, hue_sample=hue_sample, cs_name=cs_name) np.save( f"./lut/lut_sample_{ll_num}_{hue_sample}_{chroma_sample}_{cs_name}.npy", lut)
# 任意の Hue, Lightness の Gamut Boundary の LCH値を取得 from create_gamut_booundary_lut import calc_chroma_boundary_lut,\ get_gamut_boundary_lch_from_lut import color_space as cs import numpy as np from colour.utilities import tstack if __name__ == '__main__': lut = np.load("./lut/lut_sample_1024_1024_32768_ITU-R BT.2020.npy") hue = 40 lightness_num = 11 lightness_array = np.linspace(0, 100, lightness_num) hue_array = np.ones_like(lightness_array) * hue lh_array = tstack([lightness_array, hue_array]) gamut_boundary_lch = get_gamut_boundary_lch_from_lut( lut=lut, lh_array=lh_array) print(gamut_boundary_lch) """ [[ 0. 0. 40. ] [ 10. 26.41562112 40. ] [ 20. 53.00088577 40. ] [ 30. 79.69424616 40. ] [ 40. 106.60514221 40. ] [ 50. 133.87300364 40. ] [ 60. 145.00828857 40. ] [ 70.00000153 99.57528305 40. ] [ 80.00000153 60.49513295 40. ] [ 90.00000076 27.6507864 40. ] [ 100. 0. 40. ]] """
BT.709/BT.2020 の Gamut Boundary が求まったら、各Hue に対して Chroma が最大となる点を探し、そこを BT.709 Cups/BT.2020 Cups とした。
さて、図9、図11 から分かるとおり、今回の Gamut Boundary の計算方法には Yellow で理論値とズレる欠点がある。 これについて改善方法を色々と検討したのだが、対応するには費用対効果が悪いと判断して Yellow のズレは修正を諦めた(残念無念)。
ちなみに、Chroma-Lightness 平面ではなく a*-b* 平面で観測するとズレは 図12のようになる。
長々と書いてきたが上記の通りに Gamut Boundary、Cups を求めることでテストパターンを作成した。 以後で、なぜ色ズレが生じてしまったのかを解説していく。
4.3. テストパターンの色ズレの原因と対策
それでは問題のあった Yellow について Chroma-Lightness 平面の様子を確認してみる。 以下の図13は全体の様子を、図14 は一部を拡大して表示したものである。 図14 を見ると分かるように、BT.709 Cups と BT.2020 Cups を結んだ直線が BT.709 の Gamut をはみ出している。 これにより意図せぬ色ズレが発生してしまっていた。
図13. Hue=104° の全体 | 図14. 左図の Lightness 90~100 の範囲を拡大して表示 |
今回はこれを防ぐために、図13 の Focal Point の値を 50~90 に制限することにした。制限した結果を 図15、図16 に示す。
図15. 修正後の Hue=104° の全体 | 図16. 左図の Lightness 90~100 の範囲を拡大して表示 |
Focal Point の上限を 90 にした理由は Yellow で BT.709 の外側に出てしまうのを防ぐためである。 下限を 50 にした理由は、本記事では詳細を省略するが DCI-P3 Cups と BT.2020 Cups を結んだ直線を考慮すると下限値も設ける必要があったからである。
各Hue に対して Focal Point の制限前後をプロットした結果を動画1 に示す。
動画1. Focal Point を制限した場合の様子 (BT.709 - BT.2020) |
また、DCI-P3 と BT.2020 のペアに対して同様のことを行った結果を 動画2 に示す。
動画2. Focal Point を制限した場合の様子 (DCI-P3 - BT.2020) |
4.4 対策後のテストパターン
このようにして対策を施したテストパターンを図17、図18 に示す。パッチの左上に付いている "□" は DCI-P3 領域であることを、"■" は BT.2020 領域であることを意味している。
図17 修正後のテストパターン(水平ブロック数32) | 図17 修正後のテストパターン(水平ブロック数128) |
ブロック数を変化させた版や4K版をまとめた zip は以下で公開している。(誰も欲しがらないと思うが)再配布さえしなければ自由に使ってもらって構わない。 なお、今回は ICC Profile(Gamma2.4, BT.2020, D65) をテストパターンに埋め込んである。注意して頂きたい。
5. 感想
今回の記事、死ぬほど書きづらかった。自分に説明するだけで精一杯。第三者に説明するとか無理ゲーやで…。