Y'CbCr spaces?
Look into Y'CbCr spaces. Much like RGB, this is a model that can be a target transform for a given RGB gamut: BT.601, BT.709, BT.2020, etc. It also has a number of other nuances. I guess, by default, the space has a range of Y' [0, 1], Cb [-0.5, 0.5], and Cr [-0.5, 0.5]. It seems in this form it might be called Y'PbPr. Traditionally though, this space is offset into positive values and scaled giving some headroom. For 8 bit values, this is often in the range of Y' [16, 235], Cb [16, 240], and Cr [16, 240]. This range will be larger for 10 bit precision, etc. For digital values (non-floating point) this values will be rounded to whole integers.
It is unclear if these range are the space's gamut or not. For instance, BT.601 is designed around the sRGB gamut. The actual sRGB gamut is tilted within this range Y'CbCr range, but does not fill the whole range. Many of the values near the corner of this space are out of the visible spectrum.
Now, we don't have to ensure digital resolution. Users can round the value to integers if needed. We have ways to serialize values in this way already. Scaling to ranges to 8 bit, 10 bit, etc. isn't really a problem, we can provide the spaces in whatever way makes sense for the gamut under evaluation.
Now, as far as the Y'CbCr gamut, we can:
- Treat them like Lab spaces and set it as unbounded with a reference range that matches the supported range, but no hard enforcement.
- We can set it as bounded, but then we have to make things like the ray trace gamut mapping support rectangular spaces in general, not just RGBish spaces. This isn't actually hard though and we can can adjust the ray trace bounding box to fit offset ranges and weirdly scaled spaces, we just can't rotate the prism. So this is easy enough to do.
- We set the Y'CbCr gamut to reference the intended gamut. So, Y'CbCr 601 would target the sRGB gamut, Y'CbCr 709 would target Rec. 709, etc.
I'm not sure the right approach, but the starting point would be to:
- Create a generic way to generate Y'CbCr spaces.
- Decide which variants of Y'CbCr we want to support: 601, 709, 2020? Then also decide whether we expose them with 8 bit range, 10 bit range. This could likely vary depending on the specific variant.
- It wouldn't hurt to make ray trace handle more generic prismatic spaces. It will loosen constraints on us even if we decide these current spaces will just gamut map to sRGB, Rec. 2020, etc.
- Lastly, figure out how we want to specify the Y'CbCr gamuts.
I actually don't think any of this is difficult, more that we just need to make decisions regarding how to present the spaces. I may be able to have a working prototype within a day or two.
Yeah, the full gamut of Y'CbCr is kind of nonsense and impractical. You easily extend past the visible gamut. The intended gamut within the range is far more useful.
This is similar to how oRGB is sized, with maximums just large enough to accommodate the gamut of sRGB. The full range is not intended to be used.
It seems like we need to treat this the same as oRGB. The way these spaces are configured is to handle components up to a limit where the underlying gamut is satisfied. The full range of Y'CbCr in all directions is not practical and I don't think it is meant to be, but it must be large enough to accommodate the gamut.
So the real options are simply:
-
The gamut is simply a transformation space that is unbounded with a suggested reference range, like how we use lab.
-
Or it's gamut check calls back to the RGB gamut it was designed for. We bind the channels to the 8, 10, or 12 bit ranges, but the actual gamut checking is done in sRGB, Rec. 709, or Rec. 2020, depending on the gamut it is based on. This is basically how we do oRGB, and it seems like the likely approach.
We've locally adjusted 3D plotting to accommodate the reference range locally as shown in the plots above with the RGB gamut floating in the transparent Y'CbCr reference range.
Gamut mapping Y'CbCr directly with the raytrace method becomes a bit more tricky, which is why having it gamut map in the RGB gamut is also a more ideal approach.
What makes this difficult is that applying the methodology to arbitrary prismatic spaces doesn't necessarily mean those spaces behave like RGB. In RGB we know that the maximum point ([1, 1, 1]) is white and the minimum is ([0, 0, 0]) black (or flipped for subtractive spaces). In a space like Y'CbCr, this is not the case, the achromatic line is actually the mid-point of the Cb and Cr range with the intensity handled by Y'. This is like a shifted Lab space with the a and b components shifted right.
You could add specific awareness of these spaces and have specialized logic, but when the real useful gamut is the RGB gamut for which the space was created, it seems silly not to just use that. The reference range just tells you that the model can hold up to 8, 10, or 12 bits (depending on how you configure it), but those ranges are by no means meant to be used to the full extent as there are many colors that generate imaginary colors or colors that far exceed the human vision. These ranges create a box big enough to cover the RGB gamut that is tilted, so there is a bit of unused space.
With that said, there are non-RGB spaces that behave like RGB spaces: RYB and CMY. These are prismatic spaces (being rectangular prisms), that are RGB-ish in configuration even if their components do not align with red, green, and blue. The key here is that these spaces do not specify a luminance component, so maybe this is a constraint we can use. CMY currently fails in ray trace, but we can fix that by having its reference back to sRGB as our CMY is just a naive transform of RGB. RYB though is a non-RGB prismatic space. By allowing a prismatic space that has no Luminance, we could likely handle it and CMY, even though results would be awful as RYB as the transform to and from RYB doesn't extend well outside of its defined gamut. I believe most prismatic spaces with no luminance would likely work if ranges are correctly defined.
Everything should be in place to just add these spaces
I think I only plan to do 709 and 2020 currently. 601 technically has slightly different chromaticity points, but is really close. Some implement it directly off sRGB, but some implement it using the proper chromaticity points. I think we'll just skip it for now.
The last think to decide is the range. Do we expose it in unaltered YPbPr form, do we expose it in YCbCr form as floats, out do we expose it in integer form, and if so, how many bits?
I think we'll implement sYCC, which is basically sRGB, using the 601 constants. We can always add true 601 at some later time if it is truly desired, but I have no intentions right now.
I think we are likely to release these as floating point ranges, like we do with sRGB. We'll document how someone can adjust the results to digital via subclassing or even with simple math.
It looks like if we want to have floating point versions, the standard form will need to apply the shifted chroma components that are scaled to the bit depth with headroom and footroom to a rectangular prism form, and then if we want to remove the headroom and footroom, we should avoid shifting it, keeping it in the Lab-like form as if you don't, you get a little undershoot on the left side and a little overshoot on the right. It's just due to how the transform works. In integer form, this stuff gets rounded and clipped off, but not in floating point form.
Thinking about this more, we may simply just have YPbPr variants which are floating point ranges and YCbCr that are snapped to integer ranges.
Doing some tests, it became apparent that in the digital form, that converting back to RGB, especially with lower bit depth, that you can be placed out of gamut. The digital form can round values up out of gamut, and no matter how much you gamut map it, the values keep snapping to being slightly out of gamut. This problem is reduced as the bit depth increases. So there are only a couple of options.
- Accept the values may be slightly out of gamut.
- In digital form, when converting back to RGB, round and clip RGB. This would simply mean we are tightly controlled in digital form.