Repository navigation
Further improve HCT conversion - #763
facelessuser wants to merge 3 commits into
Conversation
1. Use an actually measured parameter that has been optimized for the majority of values in HCT. 1.832 allows faster and more precise convergence eliminating many cases were we in the past could not reach the target epsilon of 1e-12. 2. Employ Ostrowski's method to refine the Newton correction step ensuring max number of calls to `fromCam16` remains the same and cut the max number of iterations in half. In practice, we actually call `fromCam16` less on average increasing performance as well as further ensuring we consistently reach or exceed the target epsilon of 1e-12. 3. If derivative is too close to zero or the change between J and the corrected J is too small, exit the loop and return the best result we found so far. 4. Loop iteration adjusted from <= to < allowing us to specify the number of loops instead of the number of loops - 1. In short this gives us overall faster and more precise convergence. While the max iterations are cut in half, the max number of calls to `fromCam16` in the worse case are still the same. Worst cases usually manifest outside the visible spectrum, e.g. ProPhoto RGB in the blue region, etc.
✅ Deploy Preview for colorjs ready!
To edit notification comments on pull requests, go to your Netlify project configuration. |
|
I had been annoyed that convergence was not as good in some places, even when we were in reasonable gamuts. Finally getting around to measuring the derivative of J in respect to Y really helped us pin down a much better J' approximation: print("==== Estimate c for Approximate J' Derivative: (c * y) / J ====")
# To obtain the first derivative for `J'`, we manually measure the rate of change
# at various points for `J` in relation to `y` in a CAM16 with the same environment
# that HCT uses. From this, it can be noted that if `dy / dJ` is divided by `y / J`,
# we get a fairly constant value (roughly between 1.8 - 2). From this, we have a
# rough approximation of `J' ~= K * y / J`, where we set `K` to the measured average of
# `~1.832`.
#
# It can be noted that we do not just focus on achromatic values, but
# calculate the derivative on random colors in the Rec. 2020 gamut.
total = 0
count = 0
dy = 1e-12
for _ in range(200001):
c = Color.random('rec2020').convert('xyy')
y = c['Y']
j = cam16.xyz_to_cam(c.convert('xyz-d65').coords(), env)[0]
c['Y'] = y + dy
j1 = cam16.xyz_to_cam(c.convert('xyz-d65').coords(), env)[0]
c['Y'] = y - dy
j2 = cam16.xyz_to_cam(c.convert('xyz-d65').coords(), env)[0]
# Rate of change: `dy/dj`
dydj = (2 * dy) / (j1 - j2)
# If we assume this value is mainly dependent on J and Y,
# and divide by the ratio of `Y / J`, we end up with a
# constant `c` for an equation `c * Y / J`
total += dydj * (j / y)
count += 1
print(round(total / count, 3))Using a better approximation improved results greatly, but because the first derivative is an approximation, there were a very small number of cases that converged a little slower, as 2 was a better result, but by adding something like Ostrowski's method (which allows us to not have to estimate a second derivative on top an already estimated first derivative) it refined the corrections such that even those cases were elliminated while also giving us overall quicker convergence. |
|
Failing lint action seems to be unrelated to these changes. |
Now that we have a better approximation algorithm, we can approximate J by just using a gray scale XYZ using the direct conversion of T -> Y. Also, if we are going to check if derivative is close to zero, don't invert the derivative.
|
Ended up dropping the polynomial initial J guess. With the better approximation, we can actually just use the actual Y value and calculate a J using the D65 white point. This actually ends up working better now than the polynomial. Or I should say that it works better in low-light, high-chroma cases. They both work fine for in-gamut colors, but for tone mapping, this correction is required moving forward. |
fromCam16remains the same and cut the max number of iterations in half. In practice, we actually callfromCam16less on average increasing performance as well as further ensuring we consistently reach or exceed the target epsilon of 1e-12.In short this gives us overall faster and more precise convergence. While the max iterations are cut in half, the max number of calls to
fromCam16in the worse case are still the same. Worst cases usually manifest outside the visible spectrum, e.g. ProPhoto RGB in the blue region, etc.