Make Rec. 709 display-referred and provide old scene-referred variant - #490
Conversation
|
Much like Rec. 2020, Rec. 709 is often used with the BT.1886 transfer function for a display-referred space. This makes the change to Rec. 709, but provides the old spaces as It should be noted that round trip with HCT finds a single case where the round trip back from HCT is only at 4 decimal's of precision. This isn't a surprise, and if we tested more cases, I'm sure we'd find a few more here and there. Anyway, we can keep the test the same if we bump the HCT tolerance to 1e-13 instead of 1e-12, but we might be able to gain that back some other way...or we can just set HCT round trip testing to only require 4 decimals of precision. Nothing is broken, and nothing is wrong, it's more just a reality of HCT approximation back to XYZ, and when paired with Rec. 709 with a 2.4 gamma, it just happened to land on a case that exposes this. |
|
For now, we'll bump the HCT epsilon from 2e-12 to 2e-13 and we'll run some performance testing. Requiring a little tighter threshold will obviously cause at least an additional iteration on occasions, so we'll try to measure overall impact. |
|
The cost of making the HCT threshold smaller affects all conversions. This makes sense as it is targeting greater precision on the XYZ conversion, which we would normally kick out of with less precision. This doesn't target our specific case, but helps it indirectly. An additional decimal of precision in HCT -> XYZ conversion ensures the XYZ -> Rec709 (gamma 2.4) conversion gives us 6 decimals of precision (at least in this case). But is the increase of 2 - 3 microseconds per conversion worth the price for the rare drop to 4 decimals of precision during conversion? |
a02eb67 to
9825493
Compare
078589b to
2a9f6aa
Compare
|
As a sanity check, here is the current "failing" case that does not get enough precision. We can see that the XYZ conversion does, in fact, give us 11 decimals of precision: >>> Color('indigo').convert('rec709').convert('xyz-d65').coords()
[0.06930421383061716, 0.031076643893895917, 0.21354546896536397]
>>> Color('indigo').convert('rec709').convert('hct').convert('xyz-d65').coords()
[0.06930421383171137, 0.031076643894622946, 0.21354546896769142]And we can see that those values give us 11 decimals of precision in all channels but the green channel in Rec. 709, the one close to zero. >>> Color('indigo').convert('rec709').coords()
[0.3309172010036242, 0.0, 0.535359167363628]
>>> Color('indigo').convert('rec709').convert('hct').convert('rec709')
color(--rec709 0.33092 0.00001 0.53536 / 1)
>>> Color('indigo').convert('rec709').convert('hct').convert('rec709').coords()
[0.3309172010061091, 6.826777574939795e-06, 0.5353591673659989]It actually seems both cases where we use the new BT.1886 transfer function, for both Rec. 2020 and Rec. 709 give lower round trip in the values close to zero: >>> Color('rec709', [0.29411764705882354, 0.0, 0.5098039215686274]).convert('hct').convert('rec709').coords()
[0.29411764705887256, 1.300429410043695e-06, 0.5098039215686734]
>>> Color('rec2020', [0.29411764705882354, 0.0, 0.5098039215686274]).convert('hct').convert('rec2020').coords()
[0.29411764705885607, 1.1058130928692618e-06, 0.5098039215686583]As a matter of fact, this happens when doing conversion straight through XYZ only: >>> Color('rec2020', [0.29411764705882354, 0.0, 0.5098039215686274]).convert('xyz-d65').convert('rec2020').coords()
[0.2941176470588236, -5.8275714934138606e-08, 0.5098039215686274]I would have expected the zero channel to be at least 1e-12 or something. So, maybe it's just the sqr root and inverse sqr root back forth using 2.4 gamma with values close to zero. |
|
So, yeah, floating point error coupled with the power function is what causes this issue: This shows linear space to gamma corrected Rec. 709 and back to linear. Conversion is perfect. ===> inverse
[0.29411764705882354, 0.0, 0.5098039215686274]
[0.6005517699069883, 0.0, 0.755239428529458]
===> forward
[0.6005517699069883, 0.0, 0.755239428529458]
[0.2941176470588236, 0.0, 0.5098039215686274]When we pass through XYZ, and come back as linear, we can note the zero value is very, very close to zero, but when we pass through the EOTF, the power function gives us something around ===> inverse
[0.2941176470588236, 1.0408340855860843e-17, 0.5098039215686273]
[0.6005517699069883, 8.392840329713184e-08, 0.7552394285294579]This is probably why many EOTF and OETF when close to zero, do something different. I think this is just how it works, and the HCT conversion can't compensate for that; it just emphasizes the conversion weakness as it is a little less accurate. def eotf_srgb(rgb: Vector) -> Vector:
"""
Convert an array of sRGB values in the range 0.0 - 1.0 to linear light (un-corrected) form.
https://en.wikipedia.org/wiki/SRGB
"""
result = []
for i in rgb:
# Mirror linear nature of algorithm on the negative axis
abs_i = abs(i)
if abs_i > 0.04045:
result.append(math.copysign(((abs_i + 0.055) / 1.055) ** 2.4, i))
else:
result.append(i / 12.92)
return result |
|
Yep, Adobe 98 RGB does the same thing and is why we bumped precision earlier from 1e-8 to 1e-12. It's likely it can still fall under our current threshold if the right color was picked with a channel super close to zero. We'll provide more nuanced test options to can target HCT with specific color spaces so we can ensure all other spaces have higher accuracy. |
ce22e30 to
af304cf
Compare
af304cf to
ba28581
Compare
|
@gir-bot lgtm |
No description provided.