facelessuser / coloraide

A library to aid in using colors
https://facelessuser.github.io/coloraide
MIT License
194 stars 12 forks source link

Mitigate weakness in ray trace algorithm when used with adaptive Lightness in Luv #437

Open facelessuser opened 1 month ago

facelessuser commented 1 month ago

This is not a bug, but more a limitation of the ray trace algorithm. As such, there is no requirement this be addressed, but if a performant way were discovered that could alleviate this issue, it would be nice to have.

Luv, when using adaptive lightness above 0.05 can cause non-ideal results in the dark blue region. This is a corner case that is exposed due to having chroma reduction paths that have more extreme bends, often buried within the RGB surface.

The ray trace algorithm works well with constant lightness due to the gentle curves that constant lightness creates. In most color spaces, even with adaptive lightness, we still get fairly gentle curves, and when we do get heavier bends in the chroma reduction path, the bend often presents outside the RGB surface. This is true 99% of the time.

Luv is unique in that when using higher adaptive lightness values you can get a more severe bend, and it can often occur below the RGB surface. This causes the algorithm to have issues correcting colors back onto the chroma reduction path using projection. It can cause a correction with a lightness out of range. When the lightness falls below range, it can actually stress the color space as some do not convert low lightness and high chroma very well. Luv is one of these spaces and can cause the hue to rotate 180˚ at times when converting back to an RGB space.

We can see here when we use an adaptive value of 5 on color(rec2020 0 0 0.5) that the main bend is beneath the RGB service and is a bit more sharp. We've disabled dynamically adjusting the anchor closer to the RGB surface as it makes the example a little more confusing.

nocorrection

When we break it down, we can see better what goes wrong. We get corrected values outside the lightness range with very high chroma. What is particularly troublesome is the low lightness with high chroma as it falls upon the chroma reduction path past the achromatic point. Luv does not handle this well and takes the low light, high chroma color(--lchuv 9.24 69.251 263.58 / 1) and converts it to RGB and flips the RGB hue so that blue is now negative color(srgb-linear 0.05651 0.03396 -0.36014 / 1). The next round LChuv yielding hues of 83 instead of 263 which flips us into a more yellow color. Everything is off the rails at that point.

Initial: color(--lchuv 12.861 57.624 263.58 / 1)
Anchor: color(--lchuv 30.809 0 263.58 / 1)
----
Uncorrected: color(--lchuv 18.648 62.818 263.58 / 1)
Corrected: color(--lchuv 11.898 60.715 263.58 / 1)
Corrected RGB: color(srgb-linear -0.04286 -0.01515 0.46903 / 1)
----
Uncorrected: color(--lchuv 21.711 73.135 263.58 / 1)
Corrected: color(--lchuv 9.24 69.251 263.58 / 1)
Corrected RGB: color(srgb-linear 0.05651 0.03396 -0.36014 / 1)
----
Uncorrected: color(--lchuv 28.684 31.584 83.576 / 1)
Corrected: color(--lchuv 39.588 28.188 83.576 / 1)
Corrected RGB: color(srgb-linear 0.11943 0.11485 0.03484 / 1)
----
Final: color(--lchuv 46.985 51.736 83.576 / 1)
Clipped RGB: color(srgb-linear 0.18005 0.1703 0 / 1)

Logically, there is nothing wrong with what we are doing, but it presents a non-ideal situation. To solve this, we can identify when we are projecting the color onto the chroma reduction path that it will be an extreme case and clamp the correction such that when we project the color onto the chroma reduction path vector we avoid this extreme case. This prevents the reduction from going off into the weeds and ends up an under-corrected value, but a more sane value that is at least in the ballpark. That is what we currently do.

limited

This is indeed a corner case, one that so far only crops up with Luv/LChuv and only with hue-independent adaptive lightness at high values. But I imagine some spaces in the future could do something similar. I think this is more exposing a limitation in ray tracing, one that in most cases will never be hit, but can still surface.

This issue is simply to track the possibility of finding a performant way to mitigate this issue even better if possible, if there is no way, it's fine to simply note this as an edge case that exposes the limitation of the algorithm. This by no means diminishes the usefulness of the ray trace approach, but it just pushes it a little further than it can handle.